if not modules then modules = { } end modules ['anch-pos'] = { version = 1.001, comment = "companion to anch-pos.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- We save positional information in the main utility table. Not only can we store -- much more information in Lua but it's also more efficient. In the meantime these -- files have become quite large. In some cases that get noticed by a hickup in the -- start and/or finish, but that is the price we pay for progress. -- -- This was the last module that got rid of directly setting scanners, with a little -- performance degradation but not that noticeable. It is also a module that has been -- on the (partial) redo list for a while. -- -- We can gain a little when we group positions but then we still have to deal with -- regions and cells so we either end up with lots of extra small tables pointing to -- them and/or assembling/disassembling. I played with that and rejected the idea -- until I ran into a test case where we had 50.000 one line paragraphs in an eight -- columns setup, and there we save 25 M on a 75 M tuc file. So, I played a bit more -- and we can have a solution that is of similar performance for regular documents -- (in spite of the extra overhead) but also works ok for the large files. In normal -- documents it is never a problem, but there are always exceptions to the normal and -- often these are also cases where positions are not really used but end up in the -- tuc file anyway. -- -- Currently (because we never had split tags) we do splitting at access time, which -- is sort of inefficient but still ok. Much of this mechanism comes from MkII where -- TeX was the bottleneck. -- -- By grouping we need to set more metatable so in the end that is also overhead -- but on the average we're okay because excessive serialization also comes at a price -- and this way we also delay some activities till the moment it is realy used (which -- is not always the case with positional information. We will make this transition -- stepwise so till we're done there will be inefficiencies and overhead. -- -- The pure hash based variant combined with filtering is in anch-pos.lua and previous -- lmt versions! That is the reference. local tostring, next, setmetatable, tonumber, rawget, rawset = tostring, next, setmetatable, tonumber, rawget, rawset local sort, sortedhash = table.sort, table.sortedhash local format, gmatch = string.format, string.gmatch local P, R, C, Cc, lpegmatch = lpeg.P, lpeg.R, lpeg.C, lpeg.Cc, lpeg.match local insert, remove = table.insert, table.remove local allocate = utilities.storage.allocate local setmetatableindex, setmetatablenewindex = table.setmetatableindex, table.setmetatablenewindex local report = logs.reporter("positions") local scanners = tokens.scanners local scanstring = scanners.string local scaninteger = scanners.integer local scandimen = scanners.dimen local implement = interfaces.implement local commands = commands local context = context local ctx_latelua = context.latelua local ctx_doif = commands.doif local ctx_doifelse = commands.doifelse local tex = tex local texgetdimen = tex.getdimen local texgetcount = tex.getcount local texgetinteger = tex.getintegervalue or tex.getcount local texiscount = tex.iscount local texisdimen = tex.isdimen local texsetcount = tex.setcount local texget = tex.get local texsp = tex.sp ----- texsp = string.todimen -- because we cache this is much faster but no rounding local texgetnest = tex.getnest local texgetparstate = tex.getparstate local nuts = nodes.nuts local tonut = nodes.tonut local setlink = nuts.setlink local getlist = nuts.getlist local setlist = nuts.setlist local getbox = nuts.getbox local getid = nuts.getid local getwhd = nuts.getwhd local setprop = nuts.setprop local getparstate = nuts.getparstate local hlist_code = nodes.nodecodes.hlist local par_code = nodes.nodecodes.par local find_tail = nuts.tail ----- hpack = nuts.hpack local new_latelua = nuts.pool.latelua local variables = interfaces.variables local v_text = variables.text local v_column = variables.column local pt = number.dimenfactors.pt local pts = number.pts local formatters = string.formatters local collected = allocate() local tobesaved = allocate() local positionsused = nil local jobpositions = { collected = collected, tobesaved = tobesaved, } job.positions = jobpositions local default = { -- not r and paragraphs etc __index = { x = 0, -- x position baseline y = 0, -- y position baseline w = 0, -- width h = 0, -- height d = 0, -- depth p = 0, -- page n = 0, -- paragraph ls = 0, -- leftskip rs = 0, -- rightskip hi = 0, -- hangindent ha = 0, -- hangafter hs = 0, -- hsize pi = 0, -- parindent ps = false, -- parshape dir = 0, -- obsolete r2l = false, -- righttoleft } } local f_b_tag = formatters["b:%s"] local f_e_tag = formatters["e:%s"] local f_p_tag = formatters["p:%s"] ----- f_w_tag = formatters["w:%s"] local f_region = formatters["region:%s"] local f_tag_three = formatters["%s:%s:%s"] local f_tag_two = formatters["%s:%s"] local c_realpageno = texiscount("realpageno") local d_strutht = texisdimen("strutht") local d_strutdp = texisdimen("strutdp") -- Because positions are set with a delay we cannot yet make the tree -- so that -- is a finalizer step. But, we already have a dual split. local treemode = false local treemode = true local function checkshapes(s) for p, data in next, s do local n = #data if n > 1 then local d1 = data[1] local ph = d1[2] local pd = d1[3] local xl = d1[4] local xr = d1[5] for i=2,n do local di = data[i] local h = di[2] local d = di[3] local l = di[4] local r = di[5] if r == xr then di[5] = nil if l == xl then di[4] = nil if d == pd then di[3] = nil if h == ph then di[2] = nil else ph = h end else pd, ph = d, h end else ph, pd, xl = h, d, l end else ph, pd, xl, xr = h, d, l, r end end end end end local columndata = { } local freedata = { } -- we can make these weak local syncdata = { } -- we can make these weak local columndone = false if treemode then -- At some point we can install extra ones. I actually was halfway making a more -- general installer but we have quite some distinct handling down here and it -- became messy. So I rolled that back. Also, users and modules will quite likely -- stay in the "user" namespace. -- syncpos : indirect access via helper, todo after we switch: direct setters -- free : indirect access via helper, todo after we switch: direct setters -- columnarea : indirect access via helper, todo after we switch: direct setters -- todo: keep track of total and check that against # (sanity check) local prefix_number = { "text", "textarea", "page", "p", "free", "columnarea" } local prefix_label_number = { "syncpos" } local prefix_number_rest = { "region", "b", "e" } -- no need to split: syncpos free columnarea (textarea?) local function splitter_pattern() local p_number = R("09")^1/tonumber local p_colon = P(":") local p_label = C(P(1 - p_colon)^0) local p_rest = C(P(1)^0) return C(lpeg.utfchartabletopattern(prefix_number )) * p_colon * p_number * P(-1) + C(lpeg.utfchartabletopattern(prefix_label_number)) * p_colon * (p_number + p_label) * p_colon * p_number * P(-1) + C(lpeg.utfchartabletopattern(prefix_number_rest )) * p_colon * (p_number + p_rest) + Cc("user") * p_rest end -- In the end these metatable entries are not more efficient than copying -- but it's all about making sure that the tuc file doesn't explode. columndata = { } columndone = false local deltapacking = true -- so we can see the difference -- local deltapacking = false -- so we can see the difference local function checkcommondata(v,common) if common then local i = v.i local t = common[i] if t then v.i = nil local m = t.mt if not m then setmetatable(t,default) m = { __index = t } t.mt = m end setmetatable(v,m) return end end setmetatable(v,default) end local function initializer() tobesaved = jobpositions.tobesaved collected = jobpositions.collected -- local p_splitter = splitter_pattern() -- local list = nil -- local shared = setmetatableindex(rawget(collected,"shared"),"table") local x_y_w_h_list = shared.x_y_w_h local y_w_h_d_list = shared.y_w_h_d local x_h_d_list = shared.x_h_d local x_h_d_hs_list = shared.x_h_d_hs -- columndata = setmetatableindex(function(t,k) setmetatableindex(t,"table") list = rawget(collected,"columnarea") if list then -- for tag, data in next, list do for i=1,#list do local data = list[i] columndata[data.p or 0][data.c or 0] = data checkcommondata(data,y_w_h_d_list) end end columndone = true return t[k] end) -- -- todo: use a raw collected and a weak proxy -- setmetatableindex(collected,function(t,k) if k ~= true then local prefix, one, two = lpegmatch(p_splitter,k) local list = rawget(t,prefix) if list and type(list) == "table" then local v = list[one] or false if v then if prefix == "p" then -- if deltapacking and type(v) == "number" then if type(v) == "number" then for i=one,1,-1 do local l = list[i] if type(l) ~= "number" then if not getmetatable(l) then checkcommondata(l,x_h_d_hs_list) end v = setmetatable({ y = v }, { __index = l }) list[one] = v break end end else checkcommondata(v,x_h_d_hs_list) end elseif prefix == "text" or prefix == "textarea" then if type(v) == "number" then for i=one,1,-1 do local l = list[i] if type(l) ~= "number" then if not getmetatable(l) then checkcommondata(l,x_y_w_h_list) end v = setmetatable({ p = v }, { __index = l }) list[one] = v break end end else checkcommondata(v,x_y_w_h_list) end elseif prefix == "columnarea" then if not columndone then checkcommondata(v,y_w_h_d_list) end elseif prefix == "syncpos" then -- will become an error if two then -- v = syncdata[one][two] or { } v = v[two] or { } else v = { } end -- for j=1,#v do -- checkcommondata(v[j],x_h_d_list) -- end elseif prefix == "free" then -- will become an error elseif prefix == "page" then checkcommondata(v) else checkcommondata(v) end else if prefix == "page" then for i=one,1,-1 do local data = list[i] if data then v = setmetatableindex({ free = free or false, p = p },last) list[one] = v break end end end end t[k] = v return v end end t[k] = false return false end) -- setmetatableindex(tobesaved,function(t,k) local prefix, one, two = lpegmatch(p_splitter,k) local v = rawget(t,prefix) if v and type(v) == "table" then v = v[one] if v and two then v = v[two] end return v -- or default else -- return default end end) -- setmetatablenewindex(tobesaved,function(t,k,v) local prefix, one, two = lpegmatch(p_splitter,k) local p = rawget(t,prefix) if not p then p = { } rawset(t,prefix,p) end if type(one) == "number" then -- maybe Cc(0 1 2) if #p < one then for i=#p+1,one-1 do p[i] = { } -- false end end end if two then local pone = p[one] if not pone then pone = { } p[one] = pone end if type(two) == "number" then -- maybe Cc(0 1 2) if #pone < two then for i=#pone+1,two-1 do pone[i] = { } -- false end end end pone[two] = v else p[one] = v end end) -- syncdata = setmetatableindex(function(t,category) -- p's and y's are not shared so no need to resolve local list = rawget(collected,"syncpos") local tc = list and rawget(list,category) if tc then sort(tc,function(a,b) local ap = a.p local bp = b.p if ap == bp then return b.y < a.y else return ap < bp end end) tc.start = 1 for i=1,#tc do checkcommondata(tc[i],x_h_d_list) end else tc = { } end t[category] = tc return tc end) end local function finalizer() -- We make the (possible extensive) shape lists sparse working from the end. We -- could also drop entries here that have l and r the same which saves testing -- later on. local nofpositions = 0 local nofpartials = 0 local nofdeltas = 0 -- local x_y_w_h_size = 0 local x_y_w_h_list = { } local x_y_w_h_hash = setmetatableindex(function(t,x) local y = setmetatableindex(function(t,y) local w = setmetatableindex(function(t,w) local h = setmetatableindex(function(t,h) x_y_w_h_size = x_y_w_h_size + 1 t[h] = x_y_w_h_size x_y_w_h_list[x_y_w_h_size] = { x = x, y = y, w = w, h = h } return x_y_w_h_size end) t[w] = h return h end) t[y] = w return w end) t[x] = y return y end) -- local y_w_h_d_size = 0 local y_w_h_d_list = { } local y_w_h_d_hash = setmetatableindex(function(t,y) local w = setmetatableindex(function(t,w) local h = setmetatableindex(function(t,h) local d = setmetatableindex(function(t,d) y_w_h_d_size = y_w_h_d_size + 1 t[d] = y_w_h_d_size y_w_h_d_list[y_w_h_d_size] = { y = y, w = w, h = h, d = d } return y_w_h_d_size end) t[h] = d return d end) t[w] = h return h end) t[y] = w return w end) -- local x_h_d_size = 0 local x_h_d_list = { } local x_h_d_hash = setmetatableindex(function(t,x) local h = setmetatableindex(function(t,h) local d = setmetatableindex(function(t,d) x_h_d_size = x_h_d_size + 1 t[d] = x_h_d_size x_h_d_list[x_h_d_size] = { x = x, h = h, d = d } return x_h_d_size end) t[h] = d return d end) t[x] = h return h end) -- local x_h_d_hs_size = 0 local x_h_d_hs_list = { } local x_h_d_hs_hash = setmetatableindex(function(t,x) local h = setmetatableindex(function(t,h) local d = setmetatableindex(function(t,d) local hs = setmetatableindex(function(t,hs) x_h_d_hs_size = x_h_d_hs_size + 1 t[hs] = x_h_d_hs_size x_h_d_hs_list[x_h_d_hs_size] = { x = x, h = h, d = d, hs = hs } return x_h_d_hs_size end) t[d] = hs return hs end) t[h] = d return d end) t[x] = h return h end) -- rawset(tobesaved,"shared", { x_y_w_h = x_y_w_h_list, y_w_h_d = y_w_h_d_list, x_h_d = x_h_d_list, x_h_d_hs = x_h_d_hs_list, }) -- -- If fonts can use crazy and hard to grasp packing tricks so can we. The "i" field -- refers to a shared set of values. In addition we pack some sequences. -- -- how about free -- for k, v in sortedhash(tobesaved) do if k == "p" then -- numeric local n = #v for i=1,n do local t = v[i] local hsh = x_h_d_hs_hash[t.x or 0][t.h or 0][t.d or 0][t.hs or 0] t.x = nil t.h = nil t.d = nil t.hs = nil -- not in syncpos t.i = hsh local s = t.s if s then checkshapes(s) end end if deltapacking then -- delta packing (y) local last local current for i=1,n do current = v[i] if last then for k, v in next, last do if k ~= "y" and v ~= current[k] then goto DIFFERENT end end for k, v in next, current do if k ~= "y" and v ~= last[k] then goto DIFFERENT end end v[i] = current.y or 0 nofdeltas = nofdeltas + 1 goto CONTINUE end ::DIFFERENT:: last = current ::CONTINUE:: end end -- nofpositions = nofpositions + n nofpartials = nofpartials + n elseif k == "syncpos" then -- hash for k, t in next, v do -- numeric local n = #t for j=1,n do local t = t[j] local hsh = x_h_d_hash[t.x or 0][t.h or 0][t.d or 0] t.x = nil t.h = nil t.d = nil t.i = hsh end nofpositions = nofpositions + n nofpartials = nofpartials + n end elseif k == "text" or k == "textarea" then -- numeric local n = #v for i=1,n do local t = v[i] local hsh = x_y_w_h_hash[t.x or 0][t.y or 0][t.w or 0][t.h or 0] t.x = nil t.y = nil t.w = nil t.h = nil t.i = hsh end nofpositions = nofpositions + n nofpartials = nofpartials + n if deltapacking then -- delta packing (p) local last local current for i=1,n do current = v[i] if last then for k, v in next, last do if k ~= "p" and v ~= current[k] then goto DIFFERENT end end for k, v in next, current do if k ~= "p" and v ~= last[k] then goto DIFFERENT end end v[i] = current.p or 0 nofdeltas = nofdeltas + 1 goto CONTINUE end ::DIFFERENT:: last = current ::CONTINUE:: end end elseif k == "columnarea" then -- numeric local n = #v for i=1,n do local t = v[i] local hsh = y_w_h_d_hash[t.y or 0][t.w or 0][t.h or 0][t.d or 0] t.y = nil t.w = nil t.h = nil t.d = nil t.i = hsh end nofpositions = nofpositions + n nofpartials = nofpartials + n else -- probably only b has shapes for k, t in next, v do -- no need to sort local s = t.s if s then checkshapes(s) end nofpositions = nofpositions + 1 end end end statistics.register("positions", function() if nofpositions > 0 then return format("%s collected, %i deltas, %i shared partials, %i partial entries", nofpositions, nofdeltas, nofpartials, x_y_w_h_size + y_w_h_d_size + x_h_d_size + x_h_d_hs_size ) else return nil end end) end freedata = setmetatableindex(function(t,page) local list = rawget(collected,"free") local free = { } if list then local size = 0 for i=1,#list do local l = list[i] if l.p == page then size = size + 1 free[size] = l checkcommondata(l) end end sort(free,function(a,b) return b.y < a.y end) -- order matters ! end t[page] = free return free end) job.register('job.positions.collected', tobesaved, initializer, finalizer) else columndata = setmetatableindex("table") -- per page freedata = setmetatableindex("table") -- per page local function initializer() tobesaved = jobpositions.tobesaved collected = jobpositions.collected -- local pagedata = { } local p_splitter = lpeg.splitat(":",true) for tag, data in next, collected do local prefix, rest = lpegmatch(p_splitter,tag) if prefix == "page" then pagedata[tonumber(rest) or 0] = data elseif prefix == "free" then local t = freedata[data.p or 0] t[#t+1] = data elseif prefix == "columnarea" then columndata[data.p or 0][data.c or 0] = data end setmetatable(data,default) end local pages = structures.pages.collected if pages then local last = nil for p=1,#pages do local region = "page:" .. p local data = pagedata[p] local free = freedata[p] if free then sort(free,function(a,b) return b.y < a.y end) -- order matters ! end if data then last = data last.free = free elseif last then local t = setmetatableindex({ free = free, p = p },last) if not collected[region] then collected[region] = t else -- something is wrong end pagedata[p] = t end end end jobpositions.pagedata = pagedata -- never used end local function finalizer() -- We make the (possible extensive) shape lists sparse working from the end. We -- could also drop entries here that have l and r the same which saves testing -- later on. local nofpositions = 0 for k, v in next, tobesaved do local s = v.s if s then checkshapes(s) end nofpositions = nofpositions + 1 end statistics.register("positions", function() if nofpositions > 0 then return format("%s collected",nofpositions) else return nil end end) end local p_number = lpeg.patterns.cardinal/tonumber local p_tag = P("syncpos:") * p_number * P(":") * p_number syncdata = setmetatableindex(function(t,category) setmetatable(t,nil) for tag, pos in next, collected do local c, n = lpegmatch(p_tag,tag) if c then local tc = t[c] if tc then tc[n] = pos else t[c] = { [n] = pos } end end end for k, list in next, t do sort(list,function(a,b) local ap = a.p local bp = b.p if ap == bp then return b.y < a.y else return ap < bp end end) list.start = 1 end return t[category] end) job.register('job.positions.collected', tobesaved, initializer, finalizer) end function jobpositions.used() if positionsused == nil then positionsused = false for k, v in next, collected do if k ~= "shared" and type(v) == "table" and next(v) then positionsused = true break end end end return positionsused end function jobpositions.getfree(page) return freedata[page] end function jobpositions.getsync(category) return syncdata[category] or { } end local regions = { } local nofregions = 0 local region = nil local columns = { } local nofcolumns = 0 local column = nil local nofpages = nil -- beware ... we're not sparse here as lua will reserve slots for the nilled local getpos, gethpos, getvpos, getrpos function jobpositions.registerhandlers(t) getpos = t and t.getpos or function() return 0, 0 end getrpos = t and t.getrpos or function() return 0, 0, 0 end gethpos = t and t.gethpos or function() return 0 end getvpos = t and t.getvpos or function() return 0 end end function jobpositions.getpos () return getpos () end function jobpositions.getrpos() return getrpos() end function jobpositions.gethpos() return gethpos() end function jobpositions.getvpos() return getvpos() end -------- jobpositions.getcolumn() return column end jobpositions.registerhandlers() local function setall(name,p,x,y,w,h,d,extra) tobesaved[name] = { p = p, x = x ~= 0 and x or nil, y = y ~= 0 and y or nil, w = w ~= 0 and w or nil, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, e = extra ~= "" and extra or nil, r = region, c = column, r2l = texgetinteger("inlinelefttoright") == 1 and true or nil, } end local function enhance(data) if not data then return nil end if data.r == true then -- or "" data.r = region end if data.x == true then if data.y == true then local x, y = getpos() data.x = x ~= 0 and x or nil data.y = y ~= 0 and y or nil else local x = gethpos() data.x = x ~= 0 and x or nil end elseif data.y == true then local y = getvpos() data.y = y ~= 0 and y or nil end if data.p == true then data.p = texgetcount(c_realpageno) -- we should use a variable set in otr end if data.c == true then data.c = column end if data.w == 0 then data.w = nil end if data.h == 0 then data.h = nil end if data.d == 0 then data.d = nil end return data end -- analyze some files (with lots if margindata) and then when one key optionally -- use that one instead of a table (so, a 3rd / 4th argument: key, e.g. "x") local function set(name,index,value) -- ,key -- officially there should have been a settobesaved local data = enhance(value or {}) if value then container = tobesaved[name] if not container then tobesaved[name] = { [index] = data } else container[index] = data end else tobesaved[name] = data end end local function setspec(specification) local name = specification.name local index = specification.index local value = specification.value local data = enhance(value or {}) if value then container = tobesaved[name] if not container then tobesaved[name] = { [index] = data } else container[index] = data end else tobesaved[name] = data end end local function get(id,index) if index then local container = collected[id] return container and container[index] else return collected[id] end end ------------.setdim = setdim jobpositions.setall = setall jobpositions.set = set jobpositions.setspec = setspec jobpositions.get = get implement { name = "dosaveposition", public = true, protected = true, arguments = { "argument", "integerargument", "dimenargument", "dimenargument" }, actions = setall, -- name p x y } implement { name = "dosavepositionwhd", public = true, protected = true, arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" }, actions = setall, -- name p x y w h d } implement { name = "dosavepositionplus", public = true, protected = true, arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "argument" }, actions = setall, -- name p x y w h d extra } -- will become private table (could also become attribute driven but too nasty -- as attributes can bleed e.g. in margin stuff) -- not much gain in keeping stack (inc/dec instead of insert/remove) local function b_column(specification) local tag = specification.tag local x = gethpos() tobesaved[tag] = { r = true, x = x ~= 0 and x or nil, -- w = 0, } insert(columns,tag) column = tag end local function e_column() local t = tobesaved[column] if not t then -- something's wrong else local x = gethpos() - t.x t.w = x ~= 0 and x or nil t.r = region end remove(columns) column = columns[#columns] end jobpositions.b_column = b_column jobpositions.e_column = e_column implement { name = "bposcolumn", arguments = "string", actions = function(tag) insert(columns,tag) column = tag end } implement { name = "bposcolumnregistered", arguments = "string", actions = function(tag) insert(columns,tag) column = tag ctx_latelua { action = b_column, tag = tag } end } implement { name = "eposcolumn", actions = function() remove(columns) column = columns[#columns] end } implement { name = "eposcolumnregistered", actions = function() ctx_latelua { action = e_column } remove(columns) column = columns[#columns] end } -- regions local function b_region(specification) local tag = specification.tag or specification local last = tobesaved[tag] if last then local x, y = getpos() last.x = x ~= 0 and x or nil last.y = y ~= 0 and y or nil last.p = texgetcount(c_realpageno) insert(regions,tag) -- todo: fast stack region = tag end end local function e_region(specification) local last = tobesaved[region] if last then local y = getvpos() if specification.correct then local h = (last.y or 0) - y last.h = h ~= 0 and h or nil end last.y = y ~= 0 and y or nil remove(regions) -- todo: fast stack region = regions[#regions] end end jobpositions.b_region = b_region jobpositions.e_region = e_region local lastregion local function setregionbox(n,tag,index,k,lo,ro,to,bo,column) -- kind if not tag or tag == "" then nofregions = nofregions + 1 tag = "region" index = nofregions elseif index ~= 0 then -- So we can cheat and pass a zero index and enforce tag as is needed in -- cases where we fallback on automated region tagging (framed). tag = tag .. ":" .. index end local box = getbox(n) local w, h, d = getwhd(box) -- We could set directly but then we also need to check for gaps but as this -- is direct is is unlikely that we get a gap. We then also need to intecept -- these auto regions (comning from framed). Too messy and the split in the -- setter is fast enough. tobesaved[tag] = { -- p = texgetcount(c_realpageno), -- we copy them x = 0, y = 0, w = w ~= 0 and w or nil, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, k = k ~= 0 and k or nil, lo = lo ~= 0 and lo or nil, ro = ro ~= 0 and ro or nil, to = to ~= 0 and to or nil, bo = bo ~= 0 and bo or nil, c = column or nil, } lastregion = tag return tag, box end -- we can have a finalizer property that we catch in the backend but that demands -- a check for property for each list .. what is the impact -- textarea operates *inside* a box so experiments with pre/post hooks in the -- backend driver didn't work out (because a box can be larger) -- -- it also gives no gain to split prefix and number here because in the end we -- push and pop tags as strings, but it save a little on expansion so we do it -- in the interface local function markregionbox(n,tag,index,correct,...) -- correct needs checking local tag, box = setregionbox(n,tag,index,...) -- todo: check if tostring is needed with formatter local push = new_latelua { action = b_region, tag = tag } local pop = new_latelua { action = e_region, correct = correct } -- maybe we should construct a hbox first (needs experimenting) so that we can avoid some at the tex end local head = getlist(box) -- no, this fails with \framed[region=...] .. needs thinking -- if getid(box) ~= hlist_code then -- -- report("mark region box assumes a hlist, fix this for %a",tag) -- head = hpack(head) -- end if head then local tail = find_tail(head) setlink(push,head) setlink(tail,pop) else -- we can have a simple push/pop setlink(push,pop) end setlist(box,push) end jobpositions.markregionbox = markregionbox jobpositions.setregionbox = setregionbox function jobpositions.enhance(name) enhance(tobesaved[name]) end function jobpositions.gettobesaved(name,tag) local t = tobesaved[name] if t and tag then return t[tag] else return t end end function jobpositions.settobesaved(name,tag,data) local t = tobesaved[name] if t and tag and data then t[tag] = data end end do local c_anch_positions_paragraph = texiscount("c_anch_positions_paragraph") local nofparagraphs = 0 local function enhancepar_1(data) if data then local par = data.par -- we can pass twice when we copy local state = par and getparstate(data.par,true) if state then local x, y = getpos() if x ~= 0 then data.x = x end if y ~= 0 then data.y = y end data.p = texgetcount(c_realpageno) -- we should use a variable set in otr if column then data.c = column end if region then data.r = region end -- data.par = nil local leftskip = state.leftskip local rightskip = state.rightskip local hangindent = state.hangindent local hangafter = state.hangafter local parindent = state.parindent local parshape = state.parshape if hangafter ~= 0 and hangafter ~= 1 then data.ha = hangafter end if hangindent ~= 0 then data.hi = hangindent end data.hs = state.hsize if leftskip ~= 0 then data.ls = leftskip end if parindent ~= 0 then data.pi = parindent end if rightskip ~= 0 then data.rs = rightskip end if parshape and #parshape > 0 then data.ps = parshape end end end return data end local function enhancepar_2(data) if data then local x, y = getpos() if x ~= 0 then data.x = x end if y ~= 0 then data.y = y end data.p = texgetcount(c_realpageno) if column then data.c = column end if region then data.r = region end end return data end implement { name = "parpos", actions = function() nofparagraphs = nofparagraphs + 1 texsetcount("global",c_anch_positions_paragraph,nofparagraphs) local name = f_p_tag(nofparagraphs) local h = texgetdimen(d_strutht) local d = texgetdimen(d_strutdp) -- local top = texgetnest("top","head") local nxt = top.next if nxt then nxt = tonut(nxt) end local data if nxt and getid(nxt) == par_code then -- todo: check node type local t = { h = h, d = d, par = nxt, } tobesaved[name] = t ctx_latelua { action = enhancepar_1, specification = t } else -- This is kind of weird but it happens in tables (rows) so we probably -- need less. local state = texgetparstate() local leftskip = state.leftskip local rightskip = state.rightskip local hangindent = state.hangindent local hangafter = state.hangafter local parindent = state.parindent local parshape = state.parshape local t = { p = true, c = true, r = true, x = true, y = true, h = h, d = d, hs = state.hsize, -- never 0 } if leftskip ~= 0 then t.ls = leftskip end if rightskip ~= 0 then t.rs = rightskip end if hangindent ~= 0 then t.hi = hangindent end if hangafter ~= 1 and hangafter ~= 0 then -- can not be zero .. so it needs to be 1 if zero t.ha = hangafter end if parindent ~= 0 then t.pi = parindent end if parshape and #parshape > 0 then t.ps = parshape end tobesaved[name] = t ctx_latelua { action = enhancepar_2, specification = t } end end } implement { name = "dosetposition", arguments = "argument", public = true, protected = true, actions = function(name) local spec = { p = true, c = column, r = true, x = true, y = true, n = nofparagraphs > 0 and nofparagraphs or nil, r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } implement { name = "dosetpositionwhd", arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" }, public = true, protected = true, actions = function(name,w,h,d) local spec = { p = true, c = column, r = true, x = true, y = true, w = w ~= 0 and w or nil, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, n = nofparagraphs > 0 and nofparagraphs or nil, r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } implement { name = "dosetpositionbox", arguments = { "argument", "integerargument" }, public = true, protected = true, actions = function(name,n) local box = getbox(n) local w, h, d = getwhd(box) local spec = { p = true, c = column, r = true, x = true, y = true, w = w ~= 0 and w or nil, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, n = nofparagraphs > 0 and nofparagraphs or nil, r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } implement { name = "dosetpositionplus", arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" }, public = true, protected = true, actions = function(name,w,h,d) local spec = { p = true, c = column, r = true, x = true, y = true, w = w ~= 0 and w or nil, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, n = nofparagraphs > 0 and nofparagraphs or nil, e = scanstring(), r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } implement { name = "dosetpositionstrut", arguments = "argument", public = true, protected = true, actions = function(name) local h = texgetdimen(d_strutht) local d = texgetdimen(d_strutdp) local spec = { p = true, c = column, r = true, x = true, y = true, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, n = nofparagraphs > 0 and nofparagraphs or nil, r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } implement { name = "dosetpositionstrutkind", arguments = { "argument", "integerargument" }, public = true, protected = true, actions = function(name,kind) local h = texgetdimen(d_strutht) local d = texgetdimen(d_strutdp) local spec = { k = kind, p = true, c = column, r = true, x = true, y = true, h = h ~= 0 and h or nil, d = d ~= 0 and d or nil, n = nofparagraphs > 0 and nofparagraphs or nil, r2l = texgetinteger("inlinelefttoright") == 1 or nil, } tobesaved[name] = spec ctx_latelua { action = enhance, specification = spec } end } end function jobpositions.getreserved(tag,n) if tag == v_column then local fulltag = f_tag_three(tag,texgetcount(c_realpageno),n or 1) local data = collected[fulltag] if data then return data, fulltag end tag = v_text end if tag == v_text then local fulltag = f_tag_two(tag,texgetcount(c_realpageno)) return collected[fulltag] or false, fulltag end return collected[tag] or false, tag end function jobpositions.copy(target,source) collected[target] = collected[source] end function jobpositions.replace(id,p,x,y,w,h,d) local c = collected[id] if c then c.p = p ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ; -- c g else collected[i] = { p = p, x = x, y = y, w = w, h = h, d = d } -- c g end end local function getpage(id) local jpi = collected[id] return jpi and jpi.p end local function getcolumn(id) local jpi = collected[id] return jpi and jpi.c or false end local function getparagraph(id) local jpi = collected[id] return jpi and jpi.n end local function getregion(id) local jpi = collected[id] if jpi then local r = jpi.r if r then return r end local p = jpi.p if p then return "page:" .. p end end return false end jobpositions.page = getpage jobpositions.column = getcolumn jobpositions.paragraph = getparagraph jobpositions.region = getregion jobpositions.p = getpage -- not used, kind of obsolete jobpositions.c = getcolumn -- idem jobpositions.n = getparagraph -- idem jobpositions.r = getregion -- idem function jobpositions.x(id) local jpi = collected[id] return jpi and jpi.x end function jobpositions.y(id) local jpi = collected[id] return jpi and jpi.y end function jobpositions.width(id) local jpi = collected[id] return jpi and jpi.w end function jobpositions.height(id) local jpi = collected[id] return jpi and jpi.h end function jobpositions.depth(id) local jpi = collected[id] return jpi and jpi.d end function jobpositions.whd(id) local jpi = collected[id] if jpi then return jpi.h, jpi.h, jpi.d end end function jobpositions.leftskip(id) local jpi = collected[id] return jpi and jpi.ls end function jobpositions.rightskip(id) local jpi = collected[id] return jpi and jpi.rs end function jobpositions.hsize(id) local jpi = collected[id] return jpi and jpi.hs end function jobpositions.parindent(id) local jpi = collected[id] return jpi and jpi.pi end function jobpositions.hangindent(id) local jpi = collected[id] return jpi and jpi.hi end function jobpositions.hangafter(id) local jpi = collected[id] return jpi and jpi.ha or 1 end function jobpositions.xy(id) local jpi = collected[id] if jpi then return jpi.x, jpi.y else return 0, 0 end end function jobpositions.lowerleft(id) local jpi = collected[id] if jpi then return jpi.x, jpi.y - jpi.d else return 0, 0 end end function jobpositions.lowerright(id) local jpi = collected[id] if jpi then return jpi.x + jpi.w, jpi.y - jpi.d else return 0, 0 end end function jobpositions.upperright(id) local jpi = collected[id] if jpi then return jpi.x + jpi.w, jpi.y + jpi.h else return 0, 0 end end function jobpositions.upperleft(id) local jpi = collected[id] if jpi then return jpi.x, jpi.y + jpi.h else return 0, 0 end end function jobpositions.position(id) local jpi = collected[id] if jpi then return jpi.p, jpi.x, jpi.y, jpi.w, jpi.h, jpi.d else return 0, 0, 0, 0, 0, 0 end end local splitter = lpeg.splitat(",") function jobpositions.extra(id,n,default) -- assume numbers local jpi = collected[id] if jpi then local e = jpi.e if e then local split = jpi.split if not split then split = lpegmatch(splitter,jpi.e) jpi.split = split end return texsp(split[n]) or default -- watch the texsp here end end return default end local function overlapping(one,two,overlappingmargin) -- hm, strings so this is wrong .. texsp one = collected[one] two = collected[two] if one and two and one.p == two.p then if not overlappingmargin then overlappingmargin = 2 end local x_one = one.x local x_two = two.x local w_two = two.w local llx_one = x_one - overlappingmargin local urx_two = x_two + w_two + overlappingmargin if llx_one > urx_two then return false end local w_one = one.w local urx_one = x_one + w_one + overlappingmargin local llx_two = x_two - overlappingmargin if urx_one < llx_two then return false end local y_one = one.y local y_two = two.y local d_one = one.d local h_two = two.h local lly_one = y_one - d_one - overlappingmargin local ury_two = y_two + h_two + overlappingmargin if lly_one > ury_two then return false end local h_one = one.h local d_two = two.d local ury_one = y_one + h_one + overlappingmargin local lly_two = y_two - d_two - overlappingmargin if ury_one < lly_two then return false end return true end end local function onsamepage(list,page) for id in gmatch(list,"([^,%s]+)") do local jpi = collected[id] if jpi then local p = jpi.p if not p then return false elseif not page then page = p elseif page ~= p then return false end end end return page end local function columnofpos(realpage,xposition) local p = columndata[realpage] if p then for i=1,#p do local c = p[i] local x = c.x or 0 local w = c.w or 0 if xposition >= x and xposition <= (x + w) then return i end end end return 1 end local function getcolumndata(realpage,column) local p = columndata[realpage] if p then return p[column] end end jobpositions.overlapping = overlapping jobpositions.onsamepage = onsamepage jobpositions.columnofpos = columnofpos jobpositions.getcolumndata = getcolumndata -- interface implement { name = "replacepospxywhd", arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" }, public = true, protected = true, actions = function(name,page,x,y,w,h,d) local c = collected[name] if c then c.p = page ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ; else collected[name] = { p = page, x = x, y = y, w = w, h = h, d = d } end end } implement { name = "copyposition", arguments = "2 arguments", public = true, protected = true, actions = function(target,source) collected[target] = collected[source] end } implement { name = "MPp", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local p = jpi.p if p and p ~= true then context(p) return end end context('0') end } implement { name = "MPx", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local x = jpi.x if x and x ~= true and x ~= 0 then context("%.5Fpt",x*pt) return end end context('0pt') end } implement { name = "MPy", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local y = jpi.y if y and y ~= true and y ~= 0 then context("%.5Fpt",y*pt) return end end context('0pt') end } implement { name = "MPw", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local w = jpi.w if w and w ~= 0 then context("%.5Fpt",w*pt) return end end context('0pt') end } implement { name = "MPh", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local h = jpi.h if h and h ~= 0 then context("%.5Fpt",h*pt) return end end context('0pt') end } implement { name = "MPd", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local d = jpi.d if d and d ~= 0 then context("%.5Fpt",d*pt) return end end context('0pt') end } implement { name = "MPxy", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context('(%.5Fpt,%.5Fpt)', jpi.x*pt, jpi.y*pt ) else context('(0,0)') end end } implement { name = "MPwhd", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local w = jpi.w or 0 local h = jpi.h or 0 local d = jpi.d or 0 if w ~= 0 or h ~= 0 or d ~= 0 then context("%.5Fpt,%.5Fpt,%.5Fpt",w*pt,h*pt,d*pt) return end end context('0pt,0pt,0pt') end } implement { name = "MPll", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context('(%.5Fpt,%.5Fpt)', jpi.x *pt, (jpi.y-jpi.d)*pt ) else context('(0,0)') -- for mp only end end } implement { name = "MPlr", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context('(%.5Fpt,%.5Fpt)', (jpi.x + jpi.w)*pt, (jpi.y - jpi.d)*pt ) else context('(0,0)') -- for mp only end end } implement { name = "MPur", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context('(%.5Fpt,%.5Fpt)', (jpi.x + jpi.w)*pt, (jpi.y + jpi.h)*pt ) else context('(0,0)') -- for mp only end end } implement { name = "MPul", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context('(%.5Fpt,%.5Fpt)', jpi.x *pt, (jpi.y + jpi.h)*pt ) else context('(0,0)') -- for mp only end end } local function MPpos(id) local jpi = collected[id] if jpi then local p = jpi.p if p then context("%s,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt", p, jpi.x*pt, jpi.y*pt, jpi.w*pt, jpi.h*pt, jpi.d*pt ) return end end context('0,0,0,0,0,0') -- for mp only end implement { name = "MPpos", arguments = "argument", public = true, actions = MPpos } implement { name = "MPn", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local n = jpi.n if n then context(n) return end end context(0) end } implement { name = "MPc", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local c = jpi.c if c and c ~= true then context(c) return end end context('0') -- okay ? end } implement { name = "MPr", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then local r = jpi.r if r and r ~= true then context(r) return end local p = jpi.p if p and p ~= true then context("page:" .. p) end end end } local function MPpardata(id) local t = collected[id] if not t then local tag = f_p_tag(id) t = collected[tag] end if t then context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%s,%.5Fpt", t.hs*pt, t.ls*pt, t.rs*pt, t.hi*pt, t.ha, t.pi*pt ) else context("0,0,0,0,0,0") -- for mp only end end implement { name = "MPpardata", arguments = "argument", public = true, actions = MPpardata } -- implement { -- name = "MPposset", -- arguments = "argument", -- public = true, -- actions = function(name) -- local b = f_b_tag(name) -- local e = f_e_tag(name) -- local w = f_w_tag(name) -- local p = f_p_tag(getparagraph(b)) -- MPpos(b) context(",") MPpos(e) context(",") MPpos(w) context(",") MPpos(p) context(",") MPpardata(p) -- end -- } implement { name = "MPls", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context("%.5Fpt",jpi.ls*pt) else context("0pt") end end } implement { name = "MPrs", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context("%.5Fpt",jpi.rs*pt) else context("0pt") end end } local splitter = lpeg.tsplitat(",") implement { name = "MPplus", arguments = { "argument", "integerargument", "argument" }, public = true, actions = function(name,n,default) local jpi = collected[name] if jpi then local e = jpi.e if e then local split = jpi.split if not split then split = lpegmatch(splitter,jpi.e) jpi.split = split end context(split[n] or default) return end end context(default) end } implement { name = "MPrest", arguments = "2 arguments", public = true, actions = function(name,default) local jpi = collected[name] context(jpi and jpi.e or default) end } implement { name = "MPxywhd", arguments = "argument", public = true, actions = function(name) local jpi = collected[name] if jpi then context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt", jpi.x*pt, jpi.y*pt, jpi.w*pt, jpi.h*pt, jpi.d*pt ) else context("0,0,0,0,0") -- for mp only end end } implement { name = "doifelseposition", arguments = "argument", public = true, protected = true, actions = function(name) ctx_doifelse(collected[name]) end } implement { name = "doifposition", arguments = "argument", public = true, protected = true, actions = function(name) ctx_doif(collected[name]) end } implement { name = "doifelsepositiononpage", arguments = { "string", "integerargument" }, public = true, protected = true, actions = function(name,p) local c = collected[name] ctx_doifelse(c and c.p == p) end } implement { name = "doifelseoverlapping", arguments = "2 arguments", public = true, protected = true, actions = function(one,two) ctx_doifelse(overlapping(one,two)) end } implement { name = "doifelsepositionsonsamepage", arguments = "argument", -- string public = true, protected = true, actions = function(list) ctx_doifelse(onsamepage(list)) end } implement { name = "doifelsepositionsonthispage", arguments = "argument", -- string public = true, protected = true, actions = function(list) ctx_doifelse(onsamepage(list,tostring(texgetcount(c_realpageno)))) end } implement { name = "doifelsepositionsused", public = true, protected = true, actions = function() ctx_doifelse(jobpositions.used()) end } implement { name = "markregionbox", arguments = "2 integers", actions = markregionbox } implement { name = "setregionbox", arguments = "2 integers", actions = setregionbox } implement { name = "markregionboxtagged", arguments = { "integer", "string", "integer" }, actions = markregionbox } implement { name = "markregionboxtaggedn", arguments = { "integer", "string", "integer", "integer" }, actions = function(box,tag,index,n) markregionbox(box,tag,index,nil,nil,nil,nil,nil,nil,n) end } implement { name = "setregionboxtagged", arguments = { "integer", "string", "integer" }, actions = setregionbox } implement { name = "markregionboxcorrected", arguments = { "integer", "string", "integer", true }, actions = markregionbox } implement { name = "markregionboxtaggedkind", arguments = { "integer", "string", "integer", "integer", "dimen", "dimen", "dimen", "dimen" }, actions = function(box,tag,index,n,d1,d2,d3,d4) markregionbox(box,tag,index,nil,n,d1,d2,d3,d4) end } implement { name = "reservedautoregiontag", public = true, actions = function() nofregions = nofregions + 1 context(f_region(nofregions)) end } -- We support the low level positional commands too: local newsavepos = nodes.pool.savepos jobpositions.lastx = 0 jobpositions.lasty = 0 implement { name = "savepos", actions = function() context(newsavepos()) end } implement { name = "lastxpos", actions = function() context(jobpositions.lastx) end } implement { name = "lastypos", actions = function() context(jobpositions.lasty) end }