anch-pos.lmt /size: 72 Kb    last modification: 2025-02-21 11:03
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
49local fromposit = posit.fromposit
50
51local report            = logs.reporter("positions")
52
53local scanstring        = tokens.scanners.string
54
55local implement         = interfaces.implement
56
57local commands          = commands
58local context           = context
59
60local ctx_latelua       = context.latelua
61
62local ctx_doif          = commands.doif
63local ctx_doifelse      = commands.doifelse
64
65local tex               = tex
66local texgetdimen       = tex.getdimen
67local texgetcount       = tex.getcount
68local texgetinteger     = tex.getintegervalue or tex.getcount
69local texiscount        = tex.iscount
70local texisdimen        = tex.isdimen
71local texsetcount       = tex.setcount
72local texget            = tex.get
73local texsp             = tex.sp
74----- texsp             = string.todimen -- because we cache this is much faster but no rounding
75local texgetnest        = tex.getnest
76local texgetparstate    = tex.getparstate
77
78local nuts              = nodes.nuts
79local tonut             = nodes.tonut
80
81local setlink           = nuts.setlink
82local getlist           = nuts.getlist
83local setlist           = nuts.setlist
84local getbox            = nuts.getbox
85local getid             = nuts.getid
86local getwhd            = nuts.getwhd
87local setprop           = nuts.setprop
88
89local getparstate       = nuts.getparstate
90
91local hlist_code        <const> = nodes.nodecodes.hlist
92local par_code          <const> = nodes.nodecodes.par
93
94local find_tail         = nuts.tail
95----- hpack             = nuts.hpack
96
97local new_latelua       = nuts.pool.latelua
98
99local v_text            <const> = interfaces.variables.text
100local v_column          <const> = interfaces.variables.column
101
102local pt                <const> = number.dimenfactors.pt
103
104local formatters        = string.formatters
105
106local collected         = allocate()
107local tobesaved         = allocate()
108local positionsused     = nil
109
110local jobpositions = {
111    collected = collected,
112    tobesaved = tobesaved,
113}
114
115job.positions = jobpositions
116
117local dimension_code <const> = tokens.values.dimension
118local integer_code   <const> = tokens.values.integer
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 <const> = texiscount("realpageno")
152local d_strutht    <const> = texisdimen("strutht")
153local d_strutdp    <const> = 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            if k then
380                local prefix, one, two = lpegmatch(p_splitter,k)
381                local v = rawget(t,prefix)
382                if v and type(v) == "table" then
383                    v = v[one]
384                    if v and two then
385                        v = v[two]
386                    end
387                    return v -- or default
388                else
389                 -- return default
390                end
391            end
392        end)
393        --
394        setmetatablenewindex(tobesaved,function(t,k,v)
395            if k then
396                local prefix, one, two = lpegmatch(p_splitter,k)
397                local p = rawget(t,prefix)
398                if not p then
399                    p = { }
400                    rawset(t,prefix,p)
401                end
402                if type(one) == "number" then -- maybe Cc(0 1 2)
403                    if #p < one then
404                        for i=#p+1,one-1 do
405                            p[i] = { } -- false
406                        end
407                    end
408                end
409                if two then
410                    local pone = p[one]
411                    if not pone then
412                        pone = { }
413                        p[one] = pone
414                    end
415                    if type(two) == "number" then -- maybe Cc(0 1 2)
416                        if #pone < two then
417                            for i=#pone+1,two-1 do
418                                pone[i] = { } -- false
419                            end
420                        end
421                    end
422                    pone[two] = v
423                else
424                    p[one] = v
425                end
426            end
427        end)
428        --
429        syncdata = setmetatableindex(function(t,category)
430            -- p's and y's are not shared so no need to resolve
431            local list = rawget(collected,"syncpos")
432            local tc = list and rawget(list,category)
433            if tc then
434                sort(tc,function(a,b)
435                    local ap = a.p
436                    local bp = b.p
437                    if ap == bp then
438                        return b.y < a.y
439                    else
440                        return ap < bp
441                    end
442                end)
443                tc.start = 1
444                for i=1,#tc do
445                    checkcommondata(tc[i],x_h_d_list)
446                end
447            else
448                tc = { }
449            end
450            t[category] = tc
451            return tc
452        end)
453    end
454
455    local function finalizer()
456
457        -- We make the (possible extensive) shape lists sparse working from the end. We
458        -- could also drop entries here that have l and r the same which saves testing
459        -- later on.
460
461        local nofpositions = 0
462        local nofpartials  = 0
463        local nofdeltas    = 0
464        --
465        local x_y_w_h_size = 0
466        local x_y_w_h_list = { }
467        local x_y_w_h_hash = setmetatableindex(function(t,x)
468            local y = setmetatableindex(function(t,y)
469                local w = setmetatableindex(function(t,w)
470                    local h = setmetatableindex(function(t,h)
471                        x_y_w_h_size = x_y_w_h_size + 1
472                        t[h] = x_y_w_h_size
473                        x_y_w_h_list[x_y_w_h_size] = { x = x, y = y, w = w, h = h }
474                        return x_y_w_h_size
475                    end)
476                    t[w] = h
477                    return h
478                end)
479                t[y] = w
480                return w
481            end)
482            t[x] = y
483            return y
484        end)
485        --
486        local y_w_h_d_size = 0
487        local y_w_h_d_list = { }
488        local y_w_h_d_hash = setmetatableindex(function(t,y)
489            local w = setmetatableindex(function(t,w)
490                local h = setmetatableindex(function(t,h)
491                    local d = setmetatableindex(function(t,d)
492                        y_w_h_d_size = y_w_h_d_size + 1
493                        t[d] = y_w_h_d_size
494                        y_w_h_d_list[y_w_h_d_size] = { y = y, w = w, h = h, d = d }
495                        return y_w_h_d_size
496                    end)
497                    t[h] = d
498                    return d
499                end)
500                t[w] = h
501                return h
502            end)
503            t[y] = w
504            return w
505        end)
506        --
507        local x_h_d_size = 0
508        local x_h_d_list = { }
509        local x_h_d_hash = setmetatableindex(function(t,x)
510            local h = setmetatableindex(function(t,h)
511                local d = setmetatableindex(function(t,d)
512                    x_h_d_size = x_h_d_size + 1
513                    t[d] = x_h_d_size
514                    x_h_d_list[x_h_d_size] = { x = x, h = h, d = d }
515                    return x_h_d_size
516                end)
517                t[h] = d
518                return d
519            end)
520            t[x] = h
521            return h
522        end)
523        --
524        local x_h_d_hs_size = 0
525        local x_h_d_hs_list = { }
526        local x_h_d_hs_hash = setmetatableindex(function(t,x)
527            local h = setmetatableindex(function(t,h)
528                local d = setmetatableindex(function(t,d)
529                    local hs = setmetatableindex(function(t,hs)
530                        x_h_d_hs_size = x_h_d_hs_size + 1
531                        t[hs] = x_h_d_hs_size
532                        x_h_d_hs_list[x_h_d_hs_size] = { x = x, h = h, d = d, hs = hs }
533                        return x_h_d_hs_size
534                    end)
535                    t[d] = hs
536                    return hs
537                end)
538                t[h] = d
539                return d
540            end)
541            t[x] = h
542            return h
543        end)
544        --
545        rawset(tobesaved,"shared", {
546            x_y_w_h  = x_y_w_h_list,
547            y_w_h_d  = y_w_h_d_list,
548            x_h_d    = x_h_d_list,
549            x_h_d_hs = x_h_d_hs_list,
550        })
551        --
552        -- If fonts can use crazy and hard to grasp packing tricks so can we. The "i" field
553        -- refers to a shared set of values. In addition we pack some sequences.
554        --
555        -- how about free
556        --
557        for k, v in sortedhash(tobesaved) do
558            if k == "p" then
559                -- numeric
560                local n = #v
561                for i=1,n do
562                    local t = v[i]
563                    local hsh = x_h_d_hs_hash[t.x or 0][t.h or 0][t.d or 0][t.hs or 0]
564                    t.x = nil
565                    t.h = nil
566                    t.d = nil
567                    t.hs = nil -- not in syncpos
568                    t.i = hsh
569                    local s = t.s
570                    if s then
571                        checkshapes(s)
572                    end
573                end
574                if deltapacking then
575                    -- delta packing (y)
576                    local last
577                    local current
578                    for i=1,n do
579                        current = v[i]
580                        if last then
581                            for k, v in next, last do
582                                if k ~= "y" and v ~= current[k] then
583                                    goto DIFFERENT
584                                end
585                            end
586                            for k, v in next, current do
587                                if k ~= "y" and v ~= last[k] then
588                                    goto DIFFERENT
589                                end
590                            end
591                            v[i] = current.y or 0
592                            nofdeltas = nofdeltas + 1
593                            goto CONTINUE
594                        end
595                      ::DIFFERENT::
596                        last = current
597                      ::CONTINUE::
598                    end
599                end
600                --
601                nofpositions = nofpositions + n
602                nofpartials  = nofpartials  + n
603            elseif k == "syncpos" then
604                -- hash
605                for k, t in next, v do
606                    -- numeric
607                    local n = #t
608                    for j=1,n do
609                        local t = t[j]
610                        local hsh = x_h_d_hash[t.x or 0][t.h or 0][t.d or 0]
611                        t.x = nil
612                        t.h = nil
613                        t.d = nil
614                        t.i = hsh
615                    end
616                    nofpositions = nofpositions + n
617                    nofpartials  = nofpartials  + n
618                end
619            elseif k == "text" or k == "textarea" then
620                -- numeric
621                local n = #v
622                for i=1,n do
623                    local t = v[i]
624                    local hsh = x_y_w_h_hash[t.x or 0][t.y or 0][t.w or 0][t.h or 0]
625                    t.x = nil
626                    t.y = nil
627                    t.w = nil
628                    t.h = nil
629                    t.i = hsh
630                end
631                nofpositions = nofpositions + n
632                nofpartials  = nofpartials  + n
633                if deltapacking then
634                    -- delta packing (p)
635                    local last
636                    local current
637                    for i=1,n do
638                        current = v[i]
639                        if last then
640                            for k, v in next, last do
641                                if k ~= "p" and v ~= current[k] then
642                                    goto DIFFERENT
643                                end
644                            end
645                            for k, v in next, current do
646                                if k ~= "p" and v ~= last[k] then
647                                    goto DIFFERENT
648                                end
649                            end
650                            v[i] = current.p or 0
651                            nofdeltas = nofdeltas + 1
652                            goto CONTINUE
653                        end
654                      ::DIFFERENT::
655                        last = current
656                      ::CONTINUE::
657                    end
658                end
659            elseif k == "columnarea" then
660                -- numeric
661                local n = #v
662                for i=1,n do
663                    local t = v[i]
664                    local hsh = y_w_h_d_hash[t.y or 0][t.w or 0][t.h or 0][t.d or 0]
665                    t.y = nil
666                    t.w = nil
667                    t.h = nil
668                    t.d = nil
669                    t.i = hsh
670                end
671                nofpositions = nofpositions + n
672                nofpartials  = nofpartials  + n
673            else -- probably only b has shapes
674                for k, t in next, v do -- no need to sort
675                    local s = t.s
676                    if s then
677                        checkshapes(s)
678                    end
679                    nofpositions = nofpositions + 1
680                end
681            end
682        end
683
684        statistics.register("positions", function()
685            if nofpositions > 0 then
686                return format("%s collected, %i deltas, %i shared partials, %i partial entries",
687                    nofpositions, nofdeltas, nofpartials,
688                    x_y_w_h_size + y_w_h_d_size + x_h_d_size + x_h_d_hs_size
689                )
690            else
691                return nil
692            end
693        end)
694
695    end
696
697    freedata = setmetatableindex(function(t,page)
698        local list = rawget(collected,"free")
699        local free = { }
700        if list then
701            local size = 0
702            for i=1,#list do
703                local l = list[i]
704                if l.p == page then
705                    size = size + 1
706                    free[size] = l
707                    checkcommondata(l)
708                end
709            end
710            sort(free,function(a,b) return b.y < a.y end) -- order matters !
711        end
712        t[page] = free
713        return free
714    end)
715
716    job.register('job.positions.collected', tobesaved, initializer, finalizer)
717
718else
719
720    columndata = setmetatableindex("table") -- per page
721    freedata   = setmetatableindex("table") -- per page
722
723    local function initializer()
724        tobesaved = jobpositions.tobesaved
725        collected = jobpositions.collected
726        --
727        local pagedata   = { }
728        local p_splitter = lpeg.splitat(":",true)
729
730        for tag, data in next, collected do
731            local prefix, rest = lpegmatch(p_splitter,tag)
732            if prefix == "page" then
733                pagedata[tonumber(rest) or 0] = data
734            elseif prefix == "free" then
735                local t = freedata[data.p or 0]
736                t[#t+1] = data
737            elseif prefix == "columnarea" then
738                columndata[data.p or 0][data.c or 0] = data
739            end
740            setmetatable(data,default)
741        end
742        local pages = structures.pages.collected
743        if pages then
744            local last = nil
745            for p=1,#pages do
746                local region = "page:" .. p
747                local data   = pagedata[p]
748                local free   = freedata[p]
749                if free then
750                    sort(free,function(a,b) return b.y < a.y end) -- order matters !
751                end
752                if data then
753                    last      = data
754                    last.free = free
755                elseif last then
756                    local t = setmetatableindex({ free = free, p = p },last)
757                    if not collected[region] then
758                        collected[region] = t
759                    else
760                        -- something is wrong
761                    end
762                    pagedata[p] = t
763                end
764            end
765        end
766        jobpositions.pagedata = pagedata -- never used
767
768    end
769
770    local function finalizer()
771
772        -- We make the (possible extensive) shape lists sparse working from the end. We
773        -- could also drop entries here that have l and r the same which saves testing
774        -- later on.
775
776        local nofpositions = 0
777
778        for k, v in next, tobesaved do
779            local s = v.s
780            if s then
781                checkshapes(s)
782            end
783            nofpositions = nofpositions + 1
784        end
785
786        statistics.register("positions", function()
787            if nofpositions > 0 then
788                return format("%s collected",nofpositions)
789            else
790                return nil
791            end
792        end)
793
794    end
795
796    local p_number = lpeg.patterns.cardinal/tonumber
797    local p_tag    = P("syncpos:") * p_number * P(":") * p_number
798
799    syncdata = setmetatableindex(function(t,category)
800        setmetatable(t,nil)
801        for tag, pos in next, collected do
802            local c, n = lpegmatch(p_tag,tag)
803            if c then
804                local tc = t[c]
805                if tc then
806                    tc[n] = pos
807                else
808                    t[c] = { [n] = pos }
809                end
810            end
811        end
812        for k, list in next, t do
813            sort(list,function(a,b)
814                local ap = a.p
815                local bp = b.p
816                if ap == bp then
817                    return b.y < a.y
818                else
819                    return ap < bp
820                end
821            end)
822            list.start = 1
823        end
824        return t[category]
825    end)
826
827    job.register('job.positions.collected', tobesaved, initializer, finalizer)
828
829end
830
831function jobpositions.used()
832    if positionsused == nil then
833        positionsused = false
834        for k, v in next, collected do
835            if k ~= "shared" and type(v) == "table" and next(v) then
836                positionsused = true
837                break
838            end
839        end
840    end
841    return positionsused
842end
843
844function jobpositions.getfree(page)
845    return freedata[page]
846end
847
848function jobpositions.getsync(category)
849    return syncdata[category] or { }
850end
851
852local regions    = { }
853local nofregions = 0
854local region     = nil
855
856local columns    = { }
857local nofcolumns = 0
858local column     = nil
859
860local nofpages   = nil
861
862-- beware ... we're not sparse here as lua will reserve slots for the nilled
863
864local getpos, gethpos, getvpos, getrpos
865
866function jobpositions.registerhandlers(t)
867    getpos  = t and t.getpos  or function() return 0, 0 end
868    getrpos = t and t.getrpos or function() return 0, 0, 0 end
869    gethpos = t and t.gethpos or function() return 0 end
870    getvpos = t and t.getvpos or function() return 0 end
871end
872
873function jobpositions.getpos () return getpos () end
874function jobpositions.getrpos() return getrpos() end
875function jobpositions.gethpos() return gethpos() end
876function jobpositions.getvpos() return getvpos() end
877
878-------- jobpositions.getcolumn() return column end
879
880jobpositions.registerhandlers()
881
882local function setall(name,p,x,y,w,h,d,extra)
883    tobesaved[name] = {
884        p = p,
885        x = x ~= 0 and x or nil,
886        y = y ~= 0 and y or nil,
887        w = w ~= 0 and w or nil,
888        h = h ~= 0 and h or nil,
889        d = d ~= 0 and d or nil,
890        e = extra ~= "" and extra or nil,
891        r = region,
892        c = column,
893        r2l = texgetinteger("inlinelefttoright") == 1 and true or nil,
894    }
895end
896
897local function enhance(data)
898    if not data then
899        return nil
900    end
901    local r = data.r
902    if r == true then -- or ""
903        data.r = region
904        r = region
905    end
906    if data.x == true then
907        if data.y == true then
908            local x, y = getpos()
909            data.x = x ~= 0 and x or nil
910            data.y = y ~= 0 and y or nil
911        else
912            local x = gethpos()
913            data.x = x ~= 0 and x or nil
914        end
915    elseif data.y == true then
916        local y = getvpos()
917        data.y = y ~= 0 and y or nil
918    end
919
920    -- This is a quick hack. We also need to handel the other text areas
921    -- end. It has to do with backgrounds (see mathincontext-setups.tex)
922
923    if r then
924        local dr = tobesaved[r]
925        if dr then
926            local sy = dr.sy
927            if sy and sy ~= 1 then
928                data.y = data.y * sy
929                data.h = data.h * sy
930                data.d = data.d * sy
931            end
932        end
933    end
934
935    -- End of hack.
936
937    if data.p == true then
938        data.p = texgetcount(c_realpageno) -- we should use a variable set in otr
939    end
940    if data.c == true then
941        data.c = column
942    end
943    if data.w == 0 then
944        data.w = nil
945    end
946    if data.h == 0 then
947        data.h = nil
948    end
949    if data.d == 0 then
950        data.d = nil
951    end
952    return data
953end
954
955-- analyze some files (with lots if margindata) and then when one key optionally
956-- use that one instead of a table (so, a 3rd / 4th argument: key, e.g. "x")
957
958local function set(name,index,value) -- ,key
959    -- officially there should have been a settobesaved
960    local data = enhance(value or {})
961    if value then
962        container = tobesaved[name]
963        if not container then
964            tobesaved[name] = {
965                [index] = data
966            }
967        else
968            container[index] = data
969        end
970    else
971        tobesaved[name] = data
972    end
973end
974
975local function setspec(specification)
976    local name  = specification.name
977    local index = specification.index
978    local value = specification.value
979    local data  = enhance(value or {})
980    if value then
981        container = tobesaved[name]
982        if not container then
983            tobesaved[name] = {
984                [index] = data
985            }
986        else
987            container[index] = data
988        end
989    else
990        tobesaved[name] = data
991    end
992end
993
994local function get(id,index)
995    if index then
996        local container = collected[id]
997        return container and container[index]
998    else
999        return collected[id]
1000    end
1001end
1002
1003------------.setdim  = setdim
1004jobpositions.setall  = setall
1005jobpositions.set     = set
1006jobpositions.setspec = setspec
1007jobpositions.get     = get
1008
1009implement {
1010    name      = "dosaveposition",
1011    public    = true,
1012    protected = true,
1013 -- arguments = { "argument", "integerargument", "dimenargument", "dimenargument" },
1014    arguments = { "argument", "integer", "dimension", "dimension" },
1015    actions   = setall, -- name p x y
1016}
1017
1018implement {
1019    name      = "dosavepositionwhd",
1020    public    = true,
1021    protected = true,
1022 -- arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" },
1023    arguments = { "argument", "integer", "dimenension", "dimension", "dimension", "dimension", "dimension" },
1024    actions   = setall, -- name p x y w h d
1025}
1026
1027implement {
1028    name      = "dosavepositionplus",
1029    public    = true,
1030    protected = true,
1031 -- arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "argument" },
1032    arguments = { "argument", "integer", "dimension", "dimension", "dimension", "dimension", "dimension", "argument" },
1033    actions   = setall,  -- name p x y w h d extra
1034}
1035
1036-- will become private table (could also become attribute driven but too nasty
1037-- as attributes can bleed e.g. in margin stuff)
1038
1039-- not much gain in keeping stack (inc/dec instead of insert/remove)
1040
1041local function b_column(specification)
1042    local tag = specification.tag
1043    local x = gethpos()
1044    tobesaved[tag] = {
1045        r = true,
1046        x = x ~= 0 and x or nil,
1047     -- w = 0,
1048    }
1049    insert(columns,tag)
1050    column = tag
1051end
1052
1053local function e_column()
1054    local t = tobesaved[column]
1055    if not t then
1056        -- something's wrong
1057    else
1058        local x = gethpos() - t.x
1059        t.w = x ~= 0 and x or nil
1060        t.r = region
1061    end
1062    remove(columns)
1063    column = columns[#columns]
1064end
1065
1066jobpositions.b_column = b_column
1067jobpositions.e_column = e_column
1068
1069implement {
1070    name      = "bposcolumn",
1071    arguments = "string",
1072    actions   = function(tag)
1073        insert(columns,tag)
1074        column = tag
1075    end
1076}
1077
1078implement {
1079    name      = "bposcolumnregistered",
1080    arguments = "string",
1081    actions   = function(tag)
1082        insert(columns,tag)
1083        column = tag
1084        ctx_latelua { action = b_column, tag = tag }
1085    end
1086}
1087
1088implement {
1089    name    = "eposcolumn",
1090    actions = function()
1091        remove(columns)
1092        column = columns[#columns]
1093    end
1094}
1095
1096implement {
1097    name    = "eposcolumnregistered",
1098    actions = function()
1099        ctx_latelua { action = e_column }
1100        remove(columns)
1101        column = columns[#columns]
1102    end
1103}
1104
1105-- regions
1106
1107local function b_region(specification)
1108    local tag  = specification.tag or specification
1109    local last = tobesaved[tag]
1110    if last then
1111        local x, y = getpos()
1112        last.x = x ~= 0 and x or nil
1113        last.y = y ~= 0 and y or nil
1114        last.p = texgetcount(c_realpageno)
1115        insert(regions,tag) -- todo: fast stack
1116        region = tag
1117    end
1118end
1119
1120local function e_region(specification)
1121    local last = tobesaved[region]
1122    if last then
1123        local y = getvpos()
1124        if specification.correct then
1125            local h = (last.y or 0) - y
1126            last.h = h ~= 0 and h or nil
1127        end
1128        last.y = y ~= 0 and y or nil
1129        remove(regions) -- todo: fast stack
1130        region = regions[#regions]
1131    end
1132end
1133
1134jobpositions.b_region = b_region
1135jobpositions.e_region = e_region
1136
1137local lastregion
1138
1139local function setregionbox(n,tag,index,depth,yscale,column,k,lo,ro,to,bo) -- no correct here
1140    if not tag or tag == "" then
1141        nofregions = nofregions + 1
1142        tag   = "region"
1143        index = nofregions
1144    elseif index ~= 0 then
1145        -- So we can cheat and pass a zero index and enforce tag as is needed in
1146        -- cases where we fallback on automated region tagging (framed).
1147        tag = tag .. ":" .. index
1148    end
1149    local box = getbox(n)
1150    local w, h, d = getwhd(box)
1151    -- We could set directly but then we also need to check for gaps but as this
1152    -- is direct is is unlikely that we get a gap. We then also need to intecept
1153    -- these auto regions (comning from framed). Too messy and the split in the
1154    -- setter is fast enough.
1155    if depth then
1156        d = d + depth
1157    end
1158    if yscale and (yscale == 0 or yscale == 1) then
1159        yscale = nil
1160    end
1161    tobesaved[tag] = {
1162     -- p  = texgetcount(c_realpageno), -- we copy them
1163        x  = 0,
1164        y  = 0,
1165        w  = w  ~= 0 and w  or nil,
1166        h  = h  ~= 0 and h  or nil,
1167        d  = d  ~= 0 and d  or nil,
1168        k  = k  ~= 0 and k  or nil,
1169        lo = lo ~= 0 and lo or nil,
1170        ro = ro ~= 0 and ro or nil,
1171        to = to ~= 0 and to or nil,
1172        bo = bo ~= 0 and bo or nil,
1173        c  = column         or nil,
1174        sy = yscale
1175    }
1176    lastregion = tag
1177    return tag, box
1178end
1179
1180-- we can have a finalizer property that we catch in the backend but that demands
1181-- a check for property for each list .. what is the impact
1182
1183-- textarea operates *inside* a box so experiments with pre/post hooks in the
1184-- backend driver didn't work out (because a box can be larger)
1185--
1186-- it also gives no gain to split prefix and number here because in the end we
1187-- push and pop tags as strings, but it save a little on expansion so we do it
1188-- in the interface
1189
1190local function markregionbox(n,tag,index,depth,yscale,column,correct,...) -- correct needs checking
1191    local tag, box = setregionbox(n,tag,index,depth,yscale,column,...)
1192     -- todo: check if tostring is needed with formatter
1193    local push = new_latelua { action = b_region, tag = tag }
1194    local pop  = new_latelua { action = e_region, correct = correct }
1195    -- maybe we should construct a hbox first (needs experimenting) so that we can avoid some at the tex end
1196    local head = getlist(box)
1197    -- no, this fails with \framed[region=...] .. needs thinking
1198 -- if getid(box) ~= hlist_code then
1199 --  -- report("mark region box assumes a hlist, fix this for %a",tag)
1200 --     head = hpack(head)
1201 -- end
1202    if head then
1203        local tail = find_tail(head)
1204        setlink(push,head)
1205        setlink(tail,pop)
1206    else -- we can have a simple push/pop
1207        setlink(push,pop)
1208    end
1209    setlist(box,push)
1210end
1211
1212jobpositions.markregionbox = markregionbox
1213jobpositions.setregionbox  = setregionbox
1214
1215function jobpositions.enhance(name)
1216    enhance(tobesaved[name])
1217end
1218
1219function jobpositions.gettobesaved(name,tag)
1220    local t = tobesaved[name]
1221    if t and tag then
1222        return t[tag]
1223    else
1224        return t
1225    end
1226end
1227
1228function jobpositions.settobesaved(name,tag,data)
1229    local t = tobesaved[name]
1230    if t and tag and data then
1231        t[tag] = data
1232    end
1233end
1234
1235do
1236
1237    local c_anch_positions_paragraph <const> = texiscount("c_anch_positions_paragraph")
1238
1239    local nofparagraphs = 0
1240
1241    local function enhancepar_1(data)
1242        if data then
1243            local par   = data.par -- we can pass twice when we copy
1244            local state = par and getparstate(data.par,true)
1245            if state then
1246                local x, y = getpos()
1247                if x ~= 0 then
1248                    data.x  = x
1249                end
1250                if y ~= 0 then
1251                    data.y  = y
1252                end
1253                data.p = texgetcount(c_realpageno) -- we should use a variable set in otr
1254                if column then
1255                    data.c = column
1256                end
1257                if region then
1258                    data.r = region
1259                end
1260                --
1261                data.par         = nil
1262                local leftskip   = state.leftskip
1263                local rightskip  = state.rightskip
1264                local hangindent = state.hangindent
1265                local hangafter  = state.hangafter
1266                local parindent  = state.parindent
1267                local parshape   = state.parshape
1268                if hangafter ~= 0 and hangafter ~= 1 then
1269                    data.ha = hangafter
1270                end
1271                if hangindent ~= 0 then
1272                    data.hi = hangindent
1273                end
1274                data.hs = state.hsize
1275                if leftskip ~= 0 then
1276                    data.ls = leftskip
1277                end
1278                if parindent ~= 0 then
1279                    data.pi = parindent
1280                end
1281                if rightskip ~= 0 then
1282                    data.rs = rightskip
1283                end
1284                if parshape and #parshape > 0 then
1285                    data.ps = parshape
1286                end
1287            end
1288        end
1289        return data
1290    end
1291
1292    local function enhancepar_2(data)
1293        if data then
1294            local x, y = getpos()
1295            if x ~= 0 then
1296                data.x  = x
1297            end
1298            if y ~= 0 then
1299                data.y  = y
1300            end
1301            data.p = texgetcount(c_realpageno)
1302            if column then
1303                data.c = column
1304            end
1305            if region then
1306                data.r = region
1307            end
1308        end
1309        return data
1310    end
1311
1312    implement {
1313        name    = "parpos",
1314        actions = function()
1315            nofparagraphs = nofparagraphs + 1
1316            texsetcount("global",c_anch_positions_paragraph,nofparagraphs)
1317            local name = f_p_tag(nofparagraphs)
1318            local h = texgetdimen(d_strutht)
1319            local d = texgetdimen(d_strutdp)
1320            --
1321            local top = texgetnest("top","head")
1322            local nxt = top.next
1323            if nxt then
1324                nxt = tonut(nxt)
1325            end
1326            local data
1327            if nxt and getid(nxt) == par_code then -- todo: check node type
1328                local t = {
1329                    h   = h,
1330                    d   = d,
1331                    par = nxt,
1332                }
1333                tobesaved[name] = t
1334                ctx_latelua { action = enhancepar_1, specification = t }
1335            else
1336                -- This is kind of weird but it happens in tables (rows) so we probably
1337                -- need less.
1338                local state      = texgetparstate()
1339                local leftskip   = state.leftskip
1340                local rightskip  = state.rightskip
1341                local hangindent = state.hangindent
1342                local hangafter  = state.hangafter
1343                local parindent  = state.parindent
1344                local parshape   = state.parshape
1345                local t = {
1346                    p  = true,
1347                    c  = true,
1348                    r  = true,
1349                    x  = true,
1350                    y  = true,
1351                    h  = h,
1352                    d  = d,
1353                    hs = state.hsize, -- never 0
1354                }
1355                if leftskip ~= 0 then
1356                    t.ls = leftskip
1357                end
1358                if rightskip ~= 0 then
1359                    t.rs = rightskip
1360                end
1361                if hangindent ~= 0 then
1362                    t.hi = hangindent
1363                end
1364                if hangafter ~= 1 and hangafter ~= 0 then -- can not be zero .. so it needs to be 1 if zero
1365                    t.ha = hangafter
1366                end
1367                if parindent ~= 0 then
1368                    t.pi = parindent
1369                end
1370                if parshape and #parshape > 0 then
1371                    t.ps = parshape
1372                end
1373                tobesaved[name] = t
1374                ctx_latelua { action = enhancepar_2, specification = t }
1375            end
1376        end
1377    }
1378
1379    implement {
1380        name      = "dosetposition",
1381        arguments = "argument",
1382        public    = true,
1383        protected = true,
1384        actions   = function(name)
1385            local spec = {
1386                p   = true,
1387                c   = column,
1388                r   = true,
1389                x   = true,
1390                y   = true,
1391                n   = nofparagraphs > 0 and nofparagraphs or nil,
1392                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1393            }
1394            tobesaved[name] = spec
1395            ctx_latelua { action = enhance, specification = spec }
1396        end
1397    }
1398
1399    implement {
1400        name      = "dosetpositionwhd",
1401     -- arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" },
1402        arguments = { "argument", "dimension", "dimension", "dimension" },
1403        public    = true,
1404        protected = true,
1405        actions   = function(name,w,h,d)
1406            local spec = {
1407                p   = true,
1408                c   = column,
1409                r   = true,
1410                x   = true,
1411                y   = true,
1412                w   = w ~= 0 and w or nil,
1413                h   = h ~= 0 and h or nil,
1414                d   = d ~= 0 and d or nil,
1415                n   = nofparagraphs > 0 and nofparagraphs or nil,
1416                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1417            }
1418            tobesaved[name] = spec
1419            ctx_latelua { action = enhance, specification = spec }
1420        end
1421    }
1422
1423    implement {
1424        name      = "dosetpositionbox",
1425     -- arguments = { "argument", "integerargument" },
1426        arguments = { "argument", "integer" },
1427        public    = true,
1428        protected = true,
1429        actions   = function(name,n)
1430            local box  = getbox(n)
1431            local w, h, d = getwhd(box)
1432            local spec = {
1433                p = true,
1434                c = column,
1435                r = true,
1436                x = true,
1437                y = true,
1438                w = w ~= 0 and w or nil,
1439                h = h ~= 0 and h or nil,
1440                d = d ~= 0 and d or nil,
1441                n = nofparagraphs > 0 and nofparagraphs or nil,
1442                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1443            }
1444            tobesaved[name] = spec
1445            ctx_latelua { action = enhance, specification = spec }
1446        end
1447    }
1448
1449    implement {
1450        name      = "dosetpositionplus",
1451     -- arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" },
1452        arguments = { "argument", "dimension", "dimension", "dimension" },
1453        public    = true,
1454        protected = true,
1455        actions   = function(name,w,h,d)
1456            local spec = {
1457                p   = true,
1458                c   = column,
1459                r   = true,
1460                x   = true,
1461                y   = true,
1462                w   = w ~= 0 and w or nil,
1463                h   = h ~= 0 and h or nil,
1464                d   = d ~= 0 and d or nil,
1465                n   = nofparagraphs > 0 and nofparagraphs or nil,
1466                e   = scanstring(),
1467                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1468            }
1469            tobesaved[name] = spec
1470            ctx_latelua { action = enhance, specification = spec }
1471        end
1472    }
1473
1474    implement {
1475        name      = "dosetpositionstrut",
1476        arguments = "argument",
1477        public    = true,
1478        protected = true,
1479        actions   = function(name)
1480            local h = texgetdimen(d_strutht)
1481            local d = texgetdimen(d_strutdp)
1482            local spec = {
1483                p   = true,
1484                c   = column,
1485                r   = true,
1486                x   = true,
1487                y   = true,
1488                h   = h ~= 0 and h or nil,
1489                d   = d ~= 0 and d or nil,
1490                n   = nofparagraphs > 0 and nofparagraphs or nil,
1491                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1492            }
1493            tobesaved[name] = spec
1494            ctx_latelua { action = enhance, specification = spec }
1495        end
1496    }
1497
1498    implement {
1499        name      = "dosetpositionstrutkind",
1500     -- arguments = { "argument", "integerargument" },
1501        arguments = { "argument", "integer" },
1502        public    = true,
1503        protected = true,
1504        actions   = function(name,kind)
1505            local h = texgetdimen(d_strutht)
1506            local d = texgetdimen(d_strutdp)
1507            local spec = {
1508                k   = kind,
1509                p   = true,
1510                c   = column,
1511                r   = true,
1512                x   = true,
1513                y   = true,
1514                h   = h ~= 0 and h or nil,
1515                d   = d ~= 0 and d or nil,
1516                n   = nofparagraphs > 0 and nofparagraphs or nil,
1517                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1518            }
1519            tobesaved[name] = spec
1520            ctx_latelua { action = enhance, specification = spec }
1521        end
1522    }
1523
1524end
1525
1526function jobpositions.getreserved(tag,n)
1527    if tag == v_column then
1528        local fulltag = f_tag_three(tag,texgetcount(c_realpageno),n or 1)
1529        local data = collected[fulltag]
1530        if data then
1531            return data, fulltag
1532        end
1533        tag = v_text
1534    end
1535    if tag == v_text then
1536        local fulltag = f_tag_two(tag,texgetcount(c_realpageno))
1537        return collected[fulltag] or false, fulltag
1538    end
1539    return collected[tag] or false, tag
1540end
1541
1542function jobpositions.copy(target,source)
1543    collected[target] = collected[source]
1544end
1545
1546function jobpositions.replace(id,p,x,y,w,h,d)
1547    local c = collected[id]
1548    if c then
1549        c.p = p ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ; -- c g
1550    else
1551        collected[i] = { p = p, x = x, y = y, w = w, h = h, d = d } -- c g
1552    end
1553end
1554
1555local function getpage(id)
1556    local jpi = collected[id]
1557    return jpi and jpi.p
1558end
1559
1560local function getcolumn(id)
1561    local jpi = collected[id]
1562    return jpi and jpi.c or false
1563end
1564
1565local function getparagraph(id)
1566    local jpi = collected[id]
1567    return jpi and jpi.n
1568end
1569
1570local function getregion(id)
1571    local jpi = collected[id]
1572    if jpi then
1573        local r = jpi.r
1574        if r then
1575            return r
1576        end
1577        local p = jpi.p
1578        if p then
1579            return "page:" .. p
1580        end
1581    end
1582    return false
1583end
1584
1585jobpositions.page      = getpage
1586jobpositions.column    = getcolumn
1587jobpositions.paragraph = getparagraph
1588jobpositions.region    = getregion
1589
1590jobpositions.p = getpage      -- not used, kind of obsolete
1591jobpositions.c = getcolumn    -- idem
1592jobpositions.n = getparagraph -- idem
1593jobpositions.r = getregion    -- idem
1594
1595function jobpositions.x(id)
1596    local jpi = collected[id]
1597    return jpi and jpi.x
1598end
1599
1600function jobpositions.y(id)
1601    local jpi = collected[id]
1602    return jpi and jpi.y
1603end
1604
1605function jobpositions.width(id)
1606    local jpi = collected[id]
1607    return jpi and jpi.w
1608end
1609
1610function jobpositions.height(id)
1611    local jpi = collected[id]
1612    return jpi and jpi.h
1613end
1614
1615function jobpositions.depth(id)
1616    local jpi = collected[id]
1617    return jpi and jpi.d
1618end
1619
1620function jobpositions.whd(id)
1621    local jpi = collected[id]
1622    if jpi then
1623        return jpi.w, jpi.h, jpi.d
1624    end
1625end
1626
1627function jobpositions.leftskip(id)
1628    local jpi = collected[id]
1629    return jpi and jpi.ls
1630end
1631
1632function jobpositions.rightskip(id)
1633    local jpi = collected[id]
1634    return jpi and jpi.rs
1635end
1636
1637function jobpositions.hsize(id)
1638    local jpi = collected[id]
1639    return jpi and jpi.hs
1640end
1641
1642function jobpositions.parindent(id)
1643    local jpi = collected[id]
1644    return jpi and jpi.pi
1645end
1646
1647function jobpositions.hangindent(id)
1648    local jpi = collected[id]
1649    return jpi and jpi.hi
1650end
1651
1652function jobpositions.hangafter(id)
1653    local jpi = collected[id]
1654    return jpi and jpi.ha or 1
1655end
1656
1657function jobpositions.xy(id)
1658    local jpi = collected[id]
1659    if jpi then
1660        return jpi.x, jpi.y
1661    else
1662        return 0, 0
1663    end
1664end
1665
1666function jobpositions.lowerleft(id)
1667    local jpi = collected[id]
1668    if jpi then
1669        return jpi.x, jpi.y - jpi.d
1670    else
1671        return 0, 0
1672    end
1673end
1674
1675function jobpositions.lowerright(id)
1676    local jpi = collected[id]
1677    if jpi then
1678        return jpi.x + jpi.w, jpi.y - jpi.d
1679    else
1680        return 0, 0
1681    end
1682end
1683
1684function jobpositions.upperright(id)
1685    local jpi = collected[id]
1686    if jpi then
1687        return jpi.x + jpi.w, jpi.y + jpi.h
1688    else
1689        return 0, 0
1690    end
1691end
1692
1693function jobpositions.upperleft(id)
1694    local jpi = collected[id]
1695    if jpi then
1696        return jpi.x, jpi.y + jpi.h
1697    else
1698        return 0, 0
1699    end
1700end
1701
1702function jobpositions.position(id)
1703    local jpi = collected[id]
1704    if jpi then
1705        return jpi.p, jpi.x, jpi.y, jpi.w, jpi.h, jpi.d
1706    else
1707        return 0, 0, 0, 0, 0, 0
1708    end
1709end
1710
1711local splitter = lpeg.splitat(",")
1712
1713function jobpositions.extra(id,n,default) -- assume numbers
1714    local jpi = collected[id]
1715    if jpi then
1716        local e = jpi.e
1717        if e then
1718            local split = jpi.split
1719            if not split then
1720                split = lpegmatch(splitter,jpi.e)
1721                jpi.split = split
1722            end
1723            return texsp(split[n]) or default -- watch the texsp here
1724        end
1725    end
1726    return default
1727end
1728
1729local function overlapping(one,two,overlappingmargin) -- hm, strings so this is wrong .. texsp
1730    one = collected[one]
1731    two = collected[two]
1732    if one and two and one.p == two.p then
1733        if not overlappingmargin then
1734            overlappingmargin = 2
1735        end
1736        local x_one = one.x
1737        local x_two = two.x
1738        local w_two = two.w
1739        local llx_one = x_one         - overlappingmargin
1740        local urx_two = x_two + w_two + overlappingmargin
1741        if llx_one > urx_two then
1742            return false
1743        end
1744        local w_one = one.w
1745        local urx_one = x_one + w_one + overlappingmargin
1746        local llx_two = x_two         - overlappingmargin
1747        if urx_one < llx_two then
1748            return false
1749        end
1750        local y_one = one.y
1751        local y_two = two.y
1752        local d_one = one.d
1753        local h_two = two.h
1754        local lly_one = y_one - d_one - overlappingmargin
1755        local ury_two = y_two + h_two + overlappingmargin
1756        if lly_one > ury_two then
1757            return false
1758        end
1759        local h_one = one.h
1760        local d_two = two.d
1761        local ury_one = y_one + h_one + overlappingmargin
1762        local lly_two = y_two - d_two - overlappingmargin
1763        if ury_one < lly_two then
1764            return false
1765        end
1766        return true
1767    end
1768end
1769
1770local function onsamepage(list,page)
1771    for id in gmatch(list,"([^,%s]+)") do
1772        local jpi = collected[id]
1773        if jpi then
1774            local p = jpi.p
1775            if not p then
1776                return false
1777            elseif not page then
1778                page = p
1779            elseif page ~= p then
1780                return false
1781            end
1782        end
1783    end
1784    return page
1785end
1786
1787local function columnofpos(realpage,xposition)
1788    local p = columndata[realpage]
1789    if p then
1790        for i=1,#p do
1791            local c = p[i]
1792            local x = c.x or 0
1793            local w = c.w or 0
1794            if xposition >= x and xposition <= (x + w) then
1795                return i
1796            end
1797        end
1798    end
1799    return 1
1800end
1801
1802local function getcolumndata(realpage,column)
1803    local p = columndata[realpage]
1804    if p then
1805        return p[column]
1806    end
1807end
1808
1809jobpositions.overlapping   = overlapping
1810jobpositions.onsamepage    = onsamepage
1811jobpositions.columnofpos   = columnofpos
1812jobpositions.getcolumndata = getcolumndata
1813
1814-- interface
1815
1816implement {
1817    name      = "replacepospxywhd",
1818 -- arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" },
1819    arguments = { "argument", "integer", "dimension", "dimension", "dimension", "dimension", "dimension" },
1820    public    = true,
1821    protected = true,
1822    actions   = function(name,page,x,y,w,h,d)
1823        local c = collected[name]
1824        if c then
1825            c.p = page ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ;
1826        else
1827            collected[name] = { p = page, x = x, y = y, w = w, h = h, d = d }
1828        end
1829    end
1830}
1831
1832implement {
1833    name      = "copyposition",
1834    arguments = "2 arguments",
1835    public    = true,
1836    protected = true,
1837    actions   = function(target,source)
1838        collected[target] = collected[source]
1839    end
1840}
1841
1842implement {
1843    name      = "MPp",
1844    arguments = "argument",
1845    public    = true,
1846    actions   = function(name)
1847        local jpi = collected[name]
1848        if jpi then
1849            local p = jpi.p
1850            if p and p ~= true then
1851                context(p)
1852                return
1853            end
1854        end
1855        context('0')
1856    end
1857}
1858
1859    implement {
1860        name      = "jobposp",
1861        arguments = "argument",
1862        public    = true,
1863        usage     = "value",
1864        actions   = function(name)
1865            local jpi = collected[name]
1866            if jpi then
1867                local p = jpi.p
1868                if p and p ~= true then
1869                    return integer_code, p
1870                end
1871            end
1872            return integer_code, 0
1873        end
1874    }
1875
1876
1877implement {
1878    name      = "MPx",
1879    arguments = "argument",
1880    public    = true,
1881    actions   = function(name)
1882        local jpi = collected[name]
1883        if jpi then
1884            local x = jpi.x
1885            if x and x ~= true and x ~= 0 then
1886                context("%.5Fpt",x*pt)
1887                return
1888            end
1889        end
1890        context('0pt')
1891    end
1892}
1893
1894    implement {
1895        name      = "jobposx",
1896        arguments = "argument",
1897        public    = true,
1898        usage     = "value",
1899        actions   = function(name)
1900            local jpi = collected[name]
1901            if jpi then
1902                local x = jpi.x
1903                if x and x ~= true and x ~= 0 then
1904                    return dimension_code, x*pt
1905                end
1906            end
1907            return dimension_code, 0
1908        end
1909    }
1910
1911implement {
1912    name      = "MPy",
1913    arguments = "argument",
1914    public    = true,
1915    actions   = function(name)
1916        local jpi = collected[name]
1917        if jpi then
1918            local y = jpi.y
1919            if y and y ~= true and y ~= 0 then
1920                context("%.5Fpt",y*pt)
1921                return
1922            end
1923        end
1924        context('0pt')
1925    end
1926}
1927
1928    implement {
1929        name      = "jobposy",
1930        arguments = "argument",
1931        public    = true,
1932        usage     = "value",
1933        actions   = function(name)
1934            local jpi = collected[name]
1935            if jpi then
1936                local y = jpi.y
1937                if y and y ~= true and y ~= 0 then
1938                    return dimension_code, y*pt
1939                end
1940            end
1941            return dimension_code, 0
1942        end
1943    }
1944
1945implement {
1946    name      = "MPw",
1947    arguments = "argument",
1948    public    = true,
1949    actions   = function(name)
1950        local jpi = collected[name]
1951        if jpi then
1952            local w = jpi.w
1953            if w and w ~= 0 then
1954                context("%.5Fpt",w*pt)
1955                return
1956            end
1957        end
1958        context('0pt')
1959    end
1960}
1961
1962    implement {
1963        name      = "jobposw",
1964        arguments = "argument",
1965        public    = true,
1966        usage     = "value",
1967        actions   = function(name)
1968            local jpi = collected[name]
1969            if jpi then
1970                local w = jpi.w
1971                if w and w ~= 0 then
1972                    return dimension_code, w*pt
1973                end
1974            end
1975            return dimension_code, 0
1976        end
1977    }
1978
1979implement {
1980    name      = "MPh",
1981    arguments = "argument",
1982    public    = true,
1983    actions   = function(name)
1984        local jpi = collected[name]
1985        if jpi then
1986            local h = jpi.h
1987            if h and h ~= 0 then
1988                context("%.5Fpt",h*pt)
1989                return
1990            end
1991        end
1992        context('0pt')
1993    end
1994}
1995
1996    implement {
1997        name      = "jobposh",
1998        arguments = "argument",
1999        public    = true,
2000        usage     = "value",
2001        actions   = function(name)
2002            local jpi = collected[name]
2003            if jpi then
2004                local w = jpi.h
2005                if h and h ~= 0 then
2006                    return dimension_code, h*pt
2007                end
2008            end
2009            return dimension_code, 0
2010        end
2011    }
2012
2013implement {
2014    name      = "MPd",
2015    arguments = "argument",
2016    public    = true,
2017    actions   = function(name)
2018        local jpi = collected[name]
2019        if jpi then
2020            local d = jpi.d
2021            if d and d ~= 0 then
2022                context("%.5Fpt",d*pt)
2023                return
2024            end
2025        end
2026        context('0pt')
2027    end
2028}
2029
2030    implement {
2031        name      = "jobposd",
2032        arguments = "argument",
2033        public    = true,
2034        usage     = "value",
2035        actions   = function(name)
2036            local jpi = collected[name]
2037            if jpi then
2038                local d = jpi.d
2039                if d and d ~= 0 then
2040                    return dimension_code, d*pt
2041                end
2042            end
2043            return dimension_code, 0
2044        end
2045    }
2046
2047implement {
2048    name      = "MPxy",
2049    arguments = "argument",
2050    public    = true,
2051    actions   = function(name)
2052        local jpi = collected[name]
2053        if jpi then
2054            context('(%.5Fpt,%.5Fpt)',
2055                jpi.x*pt,
2056                jpi.y*pt
2057            )
2058        else
2059            context('(0,0)')
2060        end
2061    end
2062}
2063
2064implement {
2065    name      = "MPwhd",
2066    arguments = "argument",
2067    public    = true,
2068    actions   = function(name)
2069        local jpi = collected[name]
2070        if jpi then
2071            local w = jpi.w or 0
2072            local h = jpi.h or 0
2073            local d = jpi.d or 0
2074            if w ~= 0 or h ~= 0 or d ~= 0 then
2075                context("%.5Fpt,%.5Fpt,%.5Fpt",w*pt,h*pt,d*pt)
2076                return
2077            end
2078        end
2079        context('0pt,0pt,0pt')
2080    end
2081}
2082
2083implement {
2084    name      = "MPll",
2085    arguments = "argument",
2086    public    = true,
2087    actions   = function(name)
2088        local jpi = collected[name]
2089        if jpi then
2090            context('(%.5Fpt,%.5Fpt)',
2091                 jpi.x       *pt,
2092                (jpi.y-jpi.d)*pt
2093            )
2094        else
2095            context('(0,0)') -- for mp only
2096        end
2097    end
2098}
2099
2100implement {
2101    name      = "MPlr",
2102    arguments = "argument",
2103    public    = true,
2104    actions   = function(name)
2105        local jpi = collected[name]
2106        if jpi then
2107            context('(%.5Fpt,%.5Fpt)',
2108                (jpi.x + jpi.w)*pt,
2109                (jpi.y - jpi.d)*pt
2110            )
2111        else
2112            context('(0,0)') -- for mp only
2113        end
2114    end
2115}
2116
2117implement {
2118    name      = "MPur",
2119    arguments = "argument",
2120    public    = true,
2121    actions   = function(name)
2122        local jpi = collected[name]
2123        if jpi then
2124            context('(%.5Fpt,%.5Fpt)',
2125                (jpi.x + jpi.w)*pt,
2126                (jpi.y + jpi.h)*pt
2127            )
2128        else
2129            context('(0,0)') -- for mp only
2130        end
2131    end
2132}
2133
2134implement {
2135    name      = "MPul",
2136    arguments = "argument",
2137    public    = true,
2138    actions   = function(name)
2139        local jpi = collected[name]
2140        if jpi then
2141            context('(%.5Fpt,%.5Fpt)',
2142                 jpi.x         *pt,
2143                (jpi.y + jpi.h)*pt
2144            )
2145        else
2146            context('(0,0)') -- for mp only
2147        end
2148    end
2149}
2150
2151local function MPpos(id)
2152    local jpi = collected[id]
2153    if jpi then
2154        local p = jpi.p
2155        if p then
2156            context("%s,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
2157                p,
2158                jpi.x*pt,
2159                jpi.y*pt,
2160                jpi.w*pt,
2161                jpi.h*pt,
2162                jpi.d*pt
2163            )
2164            return
2165        end
2166    end
2167    context('0,0,0,0,0,0') -- for mp only
2168end
2169
2170implement {
2171    name      = "MPpos",
2172    arguments = "argument",
2173    public    = true,
2174    actions   = MPpos
2175}
2176
2177implement {
2178    name      = "MPn",
2179    arguments = "argument",
2180    public    = true,
2181    actions   = function(name)
2182        local jpi = collected[name]
2183        if jpi then
2184            local n = jpi.n
2185            if n then
2186                context(n)
2187                return
2188            end
2189        end
2190        context(0)
2191    end
2192}
2193
2194implement {
2195    name      = "MPc",
2196    arguments = "argument",
2197    public    = true,
2198    actions   = function(name)
2199        local jpi = collected[name]
2200        if jpi then
2201            local c = jpi.c
2202            if c and c ~= true  then
2203                context(c)
2204                return
2205            end
2206        end
2207        context('0') -- okay ?
2208    end
2209}
2210
2211implement {
2212    name      = "MPr",
2213    arguments = "argument",
2214    public    = true,
2215    actions   = function(name)
2216        local jpi = collected[name]
2217        if jpi then
2218            local r = jpi.r
2219            if r and r ~= true  then
2220                context(r)
2221                return
2222            end
2223            local p = jpi.p
2224            if p and p ~= true then
2225                context("page:" .. p)
2226            end
2227        end
2228    end
2229}
2230
2231local function MPpardata(id)
2232    local t = collected[id]
2233    if not t then
2234        local tag = f_p_tag(id)
2235        t = collected[tag]
2236    end
2237    if t then
2238        context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%s,%.5Fpt",
2239            t.hs*pt,
2240            t.ls*pt,
2241            t.rs*pt,
2242            t.hi*pt,
2243            t.ha,
2244            t.pi*pt
2245        )
2246    else
2247        context("0,0,0,0,0,0") -- for mp only
2248    end
2249end
2250
2251implement {
2252    name      = "MPpardata",
2253    arguments = "argument",
2254    public    = true,
2255    actions   = MPpardata
2256}
2257
2258-- implement {
2259--     name      = "MPposset",
2260--     arguments = "argument",
2261--     public    = true,
2262--     actions   = function(name)
2263--         local b = f_b_tag(name)
2264--         local e = f_e_tag(name)
2265--         local w = f_w_tag(name)
2266--         local p = f_p_tag(getparagraph(b))
2267--         MPpos(b) context(",") MPpos(e) context(",") MPpos(w) context(",") MPpos(p) context(",") MPpardata(p)
2268--     end
2269-- }
2270
2271implement {
2272    name      = "MPls",
2273    arguments = "argument",
2274    public    = true,
2275    actions   = function(name)
2276        local jpi = collected[name]
2277        if jpi then
2278            context("%.5Fpt",jpi.ls*pt)
2279        else
2280            context("0pt")
2281        end
2282    end
2283}
2284
2285implement {
2286    name      = "MPrs",
2287    arguments = "argument",
2288    public    = true,
2289    actions   = function(name)
2290        local jpi = collected[name]
2291        if jpi then
2292            context("%.5Fpt",jpi.rs*pt)
2293        else
2294            context("0pt")
2295        end
2296    end
2297}
2298
2299local splitter = lpeg.tsplitat(",")
2300
2301implement {
2302    name      = "MPplus",
2303 -- arguments = { "argument", "integerargument", "argument" },
2304    arguments = { "argument", "integer", "argument" },
2305    public    = true,
2306    actions   = function(name,n,default)
2307        local jpi = collected[name]
2308        if jpi then
2309            local e = jpi.e
2310            if e then
2311                local split = jpi.split
2312                if not split then
2313                    split = lpegmatch(splitter,jpi.e)
2314                    jpi.split = split
2315                end
2316                context(split[n] or default)
2317                return
2318            end
2319        end
2320        context(default)
2321    end
2322}
2323
2324implement {
2325    name      = "MPrest",
2326    arguments = "2 arguments",
2327    public    = true,
2328    actions   = function(name,default)
2329        local jpi = collected[name]
2330        context(jpi and jpi.e or default)
2331    end
2332}
2333
2334implement {
2335    name      = "MPxywhd",
2336    arguments = "argument",
2337    public    = true,
2338    actions   = function(name)
2339        local jpi = collected[name]
2340        if jpi then
2341            context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
2342                jpi.x*pt,
2343                jpi.y*pt,
2344                jpi.w*pt,
2345                jpi.h*pt,
2346                jpi.d*pt
2347            )
2348        else
2349            context("0,0,0,0,0") -- for mp only
2350        end
2351    end
2352}
2353
2354implement {
2355    name      = "doifelseposition",
2356    arguments = "argument",
2357    public    = true,
2358    protected = true,
2359    actions   = function(name)
2360        ctx_doifelse(collected[name])
2361    end
2362}
2363
2364implement {
2365    name      = "doifposition",
2366    arguments = "argument",
2367    public    = true,
2368    protected = true,
2369    actions   = function(name)
2370        ctx_doif(collected[name])
2371    end
2372}
2373
2374implement {
2375    name      = "doifelsepositiononpage",
2376 -- arguments = { "string", "integerargument" },
2377    arguments = { "string", "integer" },
2378    public    = true,
2379    protected = true,
2380    actions   = function(name,p)
2381        local c = collected[name]
2382        ctx_doifelse(c and c.p == p)
2383    end
2384}
2385
2386implement {
2387    name      = "doifelseoverlapping",
2388    arguments = "2 arguments",
2389    public    = true,
2390    protected = true,
2391    actions   = function(one,two)
2392        ctx_doifelse(overlapping(one,two))
2393    end
2394}
2395
2396implement {
2397    name      = "doifelsepositionsonsamepage",
2398    arguments = "argument", -- string
2399    public    = true,
2400    protected = true,
2401    actions   = function(list)
2402        ctx_doifelse(onsamepage(list))
2403    end
2404}
2405
2406implement {
2407    name      = "doifelsepositionsonthispage",
2408    arguments = "argument", -- string
2409    public    = true,
2410    protected = true,
2411    actions   = function(list)
2412        ctx_doifelse(onsamepage(list,tostring(texgetcount(c_realpageno))))
2413    end
2414}
2415
2416implement {
2417    name      = "doifelsepositionsused",
2418    public    = true,
2419    protected = true,
2420    actions   = function()
2421        ctx_doifelse(jobpositions.used())
2422    end
2423}
2424
2425implement {
2426    name      = "setregionbox",
2427    arguments = "2 integers",
2428    actions   = setregionbox
2429}
2430
2431implement {
2432    name      = "setregionboxtagged",
2433    arguments = { "integer", "string", "integer", "dimension", "posit" },
2434    actions   = function(box,tag,index,depth,yscale)
2435        setregionbox(box,tag,index,depth,fromposit(yscale)) -- n text index depth yscale
2436    end
2437}
2438
2439implement {
2440    name      = "markregionbox",
2441--     arguments = "2 integers",
2442    arguments = "integer",
2443    actions   = markregionbox
2444}
2445
2446implement {
2447    name      = "markregionboxtagged",
2448    arguments = { "integer", "string", "integer", "dimension", "posit" },
2449    actions   = function(box,tag,index,depth,yscale)
2450        markregionbox(box,tag,index,depth,fromposit(yscale),false,false)
2451    end
2452}
2453
2454implement {
2455    name      = "markregionboxtaggedn",
2456    arguments = { "integer", "string", "integer", "dimension", "posit", "integer" },
2457    actions   = function(box,tag,index,depth,yscale,column)
2458        markregionbox(box,tag,index,depth,fromposit(yscale),column,false)
2459    end
2460}
2461
2462implement {
2463    name      = "markregionboxcorrected",
2464    arguments = { "integer", "string", "integer", "dimension", "posit" },
2465    actions   = function(box,tag,index,depth,yscale)
2466        markregionbox(box,tag,index,depth,fromposit(yscale),false,true)
2467    end
2468}
2469
2470implement {
2471    name      = "markregionboxtaggedkind",
2472    arguments = { "integer", "string", "integer", "dimension", "posit", "integer", "dimension", "dimension", "dimension", "dimension" },
2473    actions   = function(box,tag,index,depth,yscale,kind,d1,d2,d3,d4)
2474        markregionbox(box,tag,index,depth,fromposit(yscale),false,false,kind,d1,d2,d3,d4)
2475    end
2476}
2477
2478implement {
2479    name    = "reservedautoregiontag",
2480    public  = true,
2481    actions = function()
2482        nofregions = nofregions + 1
2483        context(f_region(nofregions))
2484    end
2485}
2486
2487-- We support the low level positional commands too:
2488
2489local newsavepos = nodes.pool.savepos
2490
2491jobpositions.lastx = 0
2492jobpositions.lasty = 0
2493
2494implement {
2495    name       = "savepos",
2496    protected = true,
2497    public    = true,
2498    actions   = function()
2499        context(newsavepos()) -- or return a node
2500    end
2501}
2502
2503implement {
2504    name      = "lastxpos",
2505    usage     = "value",
2506    protected = true,
2507    public    = true,
2508    actions   = function()
2509        return dimension_code, jobpositions.lastx
2510    end
2511}
2512
2513implement {
2514    name      = "lastypos",
2515    usage     = "value",
2516    protected = true,
2517    public    = true,
2518    actions   = function()
2519        return dimension_code, jobpositions.lasty
2520    end
2521}
2522