if not modules then modules = { } end modules ['node-ref'] = { version = 1.001, comment = "companion to node-ref.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- We supported pdf right from the start and in mkii this has resulted in -- extensive control over the links. Nowadays pdftex provides a lot more -- control over margins but as mkii supports multiple backends we stuck to -- our own mechanisms. In mkiv again we implement our own handling. Eventually -- we will even disable the pdf primitives. -- helper, will end up in luatex -- is grouplevel still used? local concat = table.concat local attributes, nodes, node = attributes, nodes, node local allocate = utilities.storage.allocate, utilities.storage.mark local mark = utilities.storage.allocate, utilities.storage.mark local nodeinjections = backends.nodeinjections local codeinjections = backends.codeinjections local cleanupreferences = false local cleanupdestinations = true local transparencies = attributes.transparencies local colors = attributes.colors local references = structures.references local tasks = nodes.tasks local trace_references = false trackers.register("nodes.references", function(v) trace_references = v end) local trace_destinations = false trackers.register("nodes.destinations", function(v) trace_destinations = v end) local trace_areas = false trackers.register("nodes.areas", function(v) trace_areas = v end) local show_references = false trackers.register("nodes.references.show", function(v) show_references = tonumber(v) or (v and 2.25 or false) end) local show_destinations = false trackers.register("nodes.destinations.show", function(v) show_destinations = tonumber(v) or (v and 2.00 or false) end) local report_reference = logs.reporter("backend","references") local report_destination = logs.reporter("backend","destinations") local report_area = logs.reporter("backend","areas") local nuts = nodes.nuts local nodepool = nuts.pool local tonode = nuts.tonode local tonut = nuts.tonut local getfield = nuts.getfield local setfield = nuts.setfield local getnext = nuts.getnext local getprev = nuts.getprev local getid = nuts.getid local getlist = nuts.getlist local getattr = nuts.getattr local setattr = nuts.setattr local getsubtype = nuts.getsubtype local hpack_list = nuts.hpack local vpack_list = nuts.vpack local list_dimensions = nuts.dimensions local traverse = nuts.traverse local find_node_tail = nuts.tail local nodecodes = nodes.nodecodes local skipcodes = nodes.skipcodes local whatcodes = nodes.whatcodes local listcodes = nodes.listcodes local hlist_code = nodecodes.hlist local vlist_code = nodecodes.vlist local glue_code = nodecodes.glue local glyph_code = nodecodes.glyph local rule_code = nodecodes.rule local whatsit_code = nodecodes.whatsit local leftskip_code = skipcodes.leftskip local rightskip_code = skipcodes.rightskip local parfillskip_code = skipcodes.parfillskip local localpar_code = whatcodes.localpar local dir_code = whatcodes.dir local line_code = listcodes.line local new_rule = nodepool.rule local new_kern = nodepool.kern local free_node = nuts.free local tosequence = nodes.tosequence local implement = interfaces.implement -- Normally a (destination) area is a box or a simple stretch if nodes but when it is -- a paragraph we hav ea problem: we cannot calculate the height well. This happens -- with footnotes or content broken across a page. local function vlist_dimensions(start,stop) local temp if stop then temp = getnext(stop) setfield(stop,"next",nil) end local v = vpack_list(start) local w = getfield(v,"width") local h = getfield(v,"height") local d = getfield(v,"depth") setfield(v,"list",nil) free_node(v) if temp then setfield(stop,"next",temp) end return w, h, d end local function hlist_dimensions(start,stop,parent) local last = stop and getnext(stop) if parent then return list_dimensions(getfield(parent,"glue_set"),getfield(parent,"glue_sign"),getfield(parent,"glue_order"),start,last) else return list_dimensions(start,last) end end local function dimensions(parent,start,stop) -- in principle we could move some to the caller local id = getid(start) if start == stop then if id == hlist_code or id == vlist_code or id == glyph_code or id == rule_code then -- or image if trace_areas then report_area("dimensions taken of %a",nodecodes[id]) end return getfield(start,"width"), getfield(parent,"height"), getfield(parent,"depth") else if trace_areas then report_area("dimensions calculated of %a",nodecodes[id]) end return hlist_dimensions(start,stop) -- one node only so simple end end local last = stop and getnext(stop) if parent then -- todo: if no prev and no next and parent -- todo: we need a a list_dimensions for a vlist if getid(parent) == vlist_code then local l = getlist(parent) local c = l local ok = false while c do if c == start then ok = true end if ok and getid(c) == hlist_code then break else c = getnext(c) end end if ok and c then if trace_areas then report_area("dimensions taken of first line in vlist") end return getfield(c,"width"), getfield(c,"height"), getfield(c,"depth"), c else if trace_areas then report_area("dimensions taken of vlist (probably wrong)") end return hlist_dimensions(start,stop,parent) end else if trace_areas then report_area("dimensions taken of range starting with %a using parent",nodecodes[id]) end return hlist_dimensions(start,stop,parent) end else if trace_areas then report_area("dimensions taken of range starting with %a",nodecodes[id]) end return hlist_dimensions(start,stop) end end -- is pardir important at all? local function inject_range(head,first,last,reference,make,stack,parent,pardir,txtdir) local width, height, depth, line = dimensions(parent,first,last) if txtdir == "+TRT" or (txtdir == "===" and pardir == "TRT") then -- KH: textdir == "===" test added width = - width end local result, resolved = make(width,height,depth,reference) if result and resolved then if line then -- special case, we only treat the first line in a vlist local l = getlist(line) if trace_areas then report_area("%s: %04i %s %s %s => w=%p, h=%p, d=%p, c=%S","line", reference,pardir or "---",txtdir or "---",tosequence(l,nil,true),width,height,depth,resolved) end setfield(line,"list",result) setfield(result,"next",l) setfield(l,"prev",result) return head, last else if head == first then if trace_areas then report_area("%s: %04i %s %s %s => w=%p, h=%p, d=%p, c=%S","head", reference,pardir or "---",txtdir or "---",tosequence(first,last,true),width,height,depth,resolved) end setfield(result,"next",first) setfield(first,"prev",result) return result, last else if trace_areas then report_area("%s: %04i %s %s %s => w=%p, h=%p, d=%p, c=%S","middle", reference,pardir or "---",txtdir or "---",tosequence(first,last,true),width,height,depth,resolved) end local prev = getprev(first) if prev then setfield(prev,"next",result) setfield(result,"prev",prev) end setfield(result,"next",first) setfield(first,"prev",result) -- if first == getnext(head) then -- setfield(head,"next",result) -- hm, weird -- end return head, last end end else return head, last end end local function inject_list(id,current,reference,make,stack,pardir,txtdir) local width = getfield(current,"width") local height = getfield(current,"height") local depth = getfield(current,"depth") local correction = 0 local moveright = false local first = getlist(current) if id == hlist_code then -- box_code line_code -- can be either an explicit hbox or a line and there is no way -- to recognize this; anyway only if ht/dp (then inline) local sr = stack[reference] if first then if sr and sr[2] then local last = find_node_tail(first) if getid(last) == glue_code and getsubtype(last) == rightskip_code then local prev = getprev(last) moveright = getid(first) == glue_code and getsubtype(first) == leftskip_code if prev and getid(prev) == glue_code and getsubtype(prev) == parfillskip_code then width = dimensions(current,first,getprev(prev)) -- maybe not current as we already take care of it else if moveright and getfield(first,"writable") then width = width - getfield(getfield(first,"spec"),"stretch") * getfield(current,"glue_set") * getfield(current,"glue_sign") end if getfield(last,"writable") then width = width - getfield(getfield(last,"spec"),"stretch") * getfield(current,"glue_set") * getfield(current,"glue_sign") end end end else -- also weird end else -- ok end correction = width else correction = height + depth height, depth = depth, height -- ugly hack, needed because pdftex backend does something funny end if pardir == "TRT" then width = - width end local result, resolved = make(width,height,depth,reference) -- todo: only when width is ok if result and resolved then if trace_areas then report_area("%s: %04i %s %s %s: w=%p, h=%p, d=%p, c=%S","box", reference,pardir or "---",txtdir or "----","[]",width,height,depth,resolved) end if not first then setfield(current,"list",result) elseif moveright then -- brr no prevs done -- result after first local n = getnext(first) setfield(result,"next",n) setfield(first,"next",result) setfield(result,"prev",first) if n then setfield(n,"prev",result) end else -- first after result setfield(result,"next",first) setfield(first,"prev",result) setfield(current,"list",result) end end end -- skip is somewhat messy local function inject_areas(head,attribute,make,stack,done,skip,parent,pardir,txtdir) -- main if head then local current, first, last, firstdir, reference = head, nil, nil, nil, nil pardir = pardir or "===" txtdir = txtdir or "===" while current do local id = getid(current) if id == hlist_code or id == vlist_code then -- see dimensions: this is tricky with split off boxes like inserts -- where we can end up with a first and last spanning lines local r = getattr(current,attribute) -- test \goto{test}[page(2)] test \gotobox{test}[page(2)] -- test \goto{\TeX}[page(2)] test \gotobox{\hbox {x} \hbox {x}}[page(2)] -- if r and (not skip or r >) skip then -- maybe no > test -- inject_list(id,current,r,make,stack,pardir,txtdir) -- end if r then if not reference then reference, first, last, firstdir = r, current, current, txtdir elseif r == reference then -- same link last = current elseif (done[reference] or 0) == 0 then if not skip or r > skip then -- maybe no > test head, current = inject_range(head,first,last,reference,make,stack,parent,pardir,firstdir) reference, first, last, firstdir = nil, nil, nil, nil end else reference, first, last, firstdir = r, current, current, txtdir end done[r] = (done[r] or 0) + 1 end local list = getlist(current) if list then local h, ok h, ok , pardir, txtdir = inject_areas(list,attribute,make,stack,done,r or skip or 0,current,pardir,txtdir) setfield(current,"list",h) end if r then done[r] = done[r] - 1 end elseif id == whatsit_code then local subtype = getsubtype(current) if subtype == localpar_code then pardir = getfield(current,"dir") elseif subtype == dir_code then txtdir = getfield(current,"dir") end elseif id == glue_code and getsubtype(current) == leftskip_code then -- any glue at the left? -- else local r = getattr(current,attribute) if not r then -- just go on, can be kerns elseif not reference then reference, first, last, firstdir = r, current, current, txtdir elseif r == reference then last = current elseif (done[reference] or 0) == 0 then -- or id == glue_code and getsubtype(current) == right_skip_code if not skip or r > skip then -- maybe no > test head, current = inject_range(head,first,last,reference,make,stack,parent,pardir,firstdir) reference, first, last, firstdir = nil, nil, nil, nil end else reference, first, last, firstdir = r, current, current, txtdir end end current = getnext(current) end if reference and (done[reference] or 0) == 0 then head = inject_range(head,first,last,reference,make,stack,parent,pardir,firstdir) end end return head, true, pardir, txtdir end local function inject_area(head,attribute,make,stack,done,parent,pardir,txtdir) -- singular ! if head then pardir = pardir or "===" txtdir = txtdir or "===" local current = head while current do local id = getid(current) if id == hlist_code or id == vlist_code then local r = getattr(current,attribute) if r and not done[r] then done[r] = true inject_list(id,current,r,make,stack,pardir,txtdir) end local list = getlist(current) if list then setfield(current,"list",(inject_area(list,attribute,make,stack,done,current,pardir,txtdir))) end elseif id == whatsit_code then local subtype = getsubtype(current) if subtype == localpar_code then pardir = getfield(current,"dir") elseif subtype == dir_code then txtdir = getfield(current,"dir") end else local r = getattr(current,attribute) if r and not done[r] then done[r] = true head, current = inject_range(head,current,current,r,make,stack,parent,pardir,txtdir) end end current = getnext(current) end end return head, true end -- tracing: todo: use predefined colors local register_color = colors.register local a_color = attributes.private('color') local a_colormodel = attributes.private('colormodel') local a_transparency = attributes.private('transparency') local u_transparency = nil local u_colors = { } local force_gray = true local function addstring(what,str,shift) --todo make a pluggable helper (in font-ctx) if str then local typesetters = nuts.typesetters if typesetters then local hashes = fonts.hashes local infofont = fonts.infofont() local emwidth = hashes.emwidths [infofont] local exheight = hashes.exheights[infofont] if what == "reference" then str = str .. " " shift = - (shift or 2.25) * exheight else str = str .. " " shift = (shift or 2) * exheight end local text = typesetters.fast_hpack(str,infofont) local rule = new_rule(emwidth/5,4*exheight,3*exheight) setfield(text,"shift",shift) return nuts.fasthpack(nuts.linked(text,rule)) -- local text = typesetters.fast_hpack(str,fonts.infofont()) -- return text end end end local function colorize(width,height,depth,n,reference,what,sr,offset) if force_gray then n = 0 end u_transparency = u_transparency or transparencies.register(nil,2,.65) local ucolor = u_colors[n] if not ucolor then if n == 1 then u_color = register_color(nil,'rgb',.75,0,0) elseif n == 2 then u_color = register_color(nil,'rgb',0,.75,0) elseif n == 3 then u_color = register_color(nil,'rgb',0,0,.75) else n = 0 u_color = register_color(nil,'gray',.5) end u_colors[n] = u_color end if width == 0 then -- probably a strut as placeholder report_area("%s %s has no %s dimensions, width %p, height %p, depth %p",what,reference,"horizontal",width,height,depth) width = 65536 end if height + depth <= 0 then report_area("%s %s has no %s dimensions, width %p, height %p, depth %p",what,reference,"vertical",width,height,depth) height = 65536/2 depth = height end local rule = new_rule(width,height,depth) -- todo: use tracer rule setattr(rule,a_colormodel,1) -- gray color model setattr(rule,a_color,u_color) setattr(rule,a_transparency,u_transparency) if width < 0 then local kern = new_kern(width) setfield(rule,"width",-width) setfield(kern,"next",rule) setfield(rule,"prev",kern) return kern else if sr and sr ~= "" then local text = addstring(what,sr,shift) if text then local kern = new_kern(-getfield(text,"width")) setfield(kern,"next",text) setfield(text,"prev",kern) setfield(text,"next",rule) setfield(rule,"prev",text) return kern end end return rule end end local function justadd(what,sr,shift) if sr and sr ~= "" then local text = addstring(what,sr,shift) if text then local kern = new_kern(-getfield(text,"width")) setfield(kern,"next",text) setfield(text,"prev",kern) setfield(text,"next",rule) setfield(rule,"prev",text) return kern end end end -- references: local texsetattribute = tex.setattribute local texsetcount = tex.setcount local stack = { } local done = { } local attribute = attributes.private('reference') local nofreferences = 0 local topofstack = 0 nodes.references = { attribute = attribute, stack = stack, done = done, } -- todo: get rid of n (n is just a number, can be used for tracing, obsolete) local function setreference(h,d,r) topofstack = topofstack + 1 -- the preroll permits us to determine samepage (but delayed also has some advantages) -- so some part of the backend work is already done here stack[topofstack] = { r, h, d, codeinjections.prerollreference(r) } -- texsetattribute(attribute,topofstack) -- todo -> at tex end texsetcount("lastreferenceattribute",topofstack) end function references.get(n) -- not public so functionality can change local sn = stack[n] return sn and sn[1] end local function makereference(width,height,depth,reference) -- height and depth are of parent local sr = stack[reference] if sr then if trace_references then report_reference("resolving attribute %a",reference) end local resolved, ht, dp, set, n = sr[1], sr[2], sr[3], sr[4], sr[5] -- logs.report("temp","child: ht=%p dp=%p, parent: ht=%p dp=%p",ht,dp,height,depth) if ht then if height < ht then height = ht end if depth < dp then depth = dp end end -- logs.report("temp","used: ht=%p dp=%p",height,depth) local annot = nodeinjections.reference(width,height,depth,set) if annot then annot = tonut(annot) -- todo nofreferences = nofreferences + 1 local result, current, texts if show_references then local d = sr[1] if d then local r = d.reference local p = d.prefix if r then if p then texts = p .. "|" .. r else texts = r end else -- t[#t+1] = d.internal or "?" end end end if trace_references then local step = 65536 result = hpack_list(colorize(width,height-step,depth-step,2,reference,"reference",texts,show_references)) -- step subtracted so that we can see seperate links setfield(result,"width",0) current = result elseif texts then texts = justadd("reference",texts,show_references) if texts then result = hpack_list(texts) setfield(result,"width",0) current = result end end if current then setfield(current,"next",annot) setfield(annot,"prev",current) else result = annot end references.registerpage(n) result = hpack_list(result,0) setfield(result,"width",0) setfield(result,"height",0) setfield(result,"depth",0) if cleanupreferences then stack[reference] = nil end return result, resolved elseif trace_references then report_reference("unable to resolve annotation %a",reference) end elseif trace_references then report_reference("unable to resolve attribute %a",reference) end end -- function nodes.references.handler(head) -- if topofstack > 0 then -- return inject_areas(head,attribute,makereference,stack,done) -- else -- return head, false -- end -- end function nodes.references.handler(head) if topofstack > 0 then head = tonut(head) local head, done = inject_areas(head,attribute,makereference,stack,done) return tonode(head), done else return head, false end end -- destinations (we can clean up once set, unless tagging!) local stack = { } local done = { } local attribute = attributes.private('destination') local nofdestinations = 0 local topofstack = 0 nodes.destinations = { attribute = attribute, stack = stack, done = done, } local function setdestination(n,h,d,name,view) -- n = grouplevel, name == table topofstack = topofstack + 1 stack[topofstack] = { n, h, d, name, view } return topofstack end local function makedestination(width,height,depth,reference) local sr = stack[reference] if sr then if trace_destinations then report_destination("resolving attribute %a",reference) end local resolved, ht, dp, name, view = sr[1], sr[2], sr[3], sr[4], sr[5] -- sr[4] will change to just internal if ht then if height < ht then height = ht end if depth < dp then depth = dp end end local result, current, texts if show_destinations then if name and #name > 0 then local t = { } for i=1,#name do local s = name[i] if type(s) == "number" then local d = references.internals[s] if d then d = d.references local r = d.reference local p = d.usedprefix if r then if p then t[#t+1] = p .. "|" .. r else t[#t+1] = r end else -- t[#t+1] = d.internal or "?" end end else -- in fact we have a prefix:name here end end if #t > 0 then texts = concat(t," & ") end end end if trace_destinations then local step = 0 if width == 0 then step = 4*65536 width, height, depth = 5*step, 5*step, 0 end local rule = hpack_list(colorize(width,height,depth,3,reference,"destination",texts,show_destinations)) setfield(rule,"width",0) if not result then result, current = rule, rule else setfield(current,"next",rule) setfield(rule,"prev",current) current = rule end width, height = width - step, height - step elseif texts then texts = justadd("destination",texts,show_destinations) if texts then result = hpack_list(texts) if result then setfield(result,"width",0) current = result end end end nofdestinations = nofdestinations + 1 local annot = nodeinjections.destination(width,height,depth,name,view) if annot then annot = tonut(annot) -- obsolete soon if result then setfield(current,"next",annot) setfield(annot,"prev",current) else result = annot end current = find_node_tail(annot) end if result then -- some internal error result = hpack_list(result,0) setfield(result,"width",0) setfield(result,"height",0) setfield(result,"depth",0) end if cleanupdestinations then stack[reference] = nil end return result, resolved elseif trace_destinations then report_destination("unable to resolve attribute %a",reference) end end -- function nodes.destinations.handler(head) -- if topofstack > 0 then -- return inject_area(head,attribute,makedestination,stack,done) -- singular -- else -- return head, false -- end -- end function nodes.destinations.handler(head) if topofstack > 0 then head = tonut(head) local head, done = inject_areas(head,attribute,makedestination,stack,done) return tonode(head), done else return head, false end end -- will move function references.mark(reference,h,d,view) return setdestination(tex.currentgrouplevel,h,d,reference,view) end function references.inject(prefix,reference,specification) -- todo: use currentreference is possible -- print(prefix,reference,h,d,highlight,newwindow,layer) local set, bug = references.identify(prefix,reference) if bug or #set == 0 then -- unknown ref, just don't set it and issue an error else -- check set.highlight = specification.highlight set.newwindow = specification.newwindow set.layer = specification.layer setreference(specification.height,specification.depth,set) -- sets attribute / todo: for set[*].error end end function references.injectcurrentset(h,d) -- used inside doifelse local currentset = references.currentset if currentset then setreference(h,d,currentset) -- sets attribute / todo: for set[*].error end end implement { name = "injectreference", actions = references.inject, arguments = { "string", "string", { { "highlight", "boolean" }, { "newwindow", "boolean" }, { "layer" }, { "height", "dimen" }, { "depth", "dimen" }, } } } implement { name = "injectcurrentreference", actions = references.injectcurrentset, } implement { name = "injectcurrentreferencehtdp", actions = references.injectcurrentset, arguments = { "dimen", "dimen" }, } -- local function checkboth(open,close) if open and open ~= "" then local set, bug = references.identify("",open) open = not bug and #set > 0 and set end if close and close ~= "" then local set, bug = references.identify("",close) close = not bug and #set > 0 and set end return open, close end -- end temp hack statistics.register("interactive elements", function() if nofreferences > 0 or nofdestinations > 0 then return string.format("%s references, %s destinations",nofreferences,nofdestinations) else return nil end end) function references.enableinteraction() tasks.enableaction("shipouts","nodes.references.handler") tasks.enableaction("shipouts","nodes.destinations.handler") function references.enableinteraction() end end implement { name = "enableinteraction", actions = references.enableinteraction, onlyonce = true }