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 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 hpack_list = node.hpack local list_dimensions = node.dimensions local trace_backend = false trackers.register("nodes.backend", function(v) trace_backend = v end) 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 report_reference = logs.reporter("backend","references") local report_destination = logs.reporter("backend","destinations") local report_area = logs.reporter("backend","areas") 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 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 nodepool = nodes.pool local new_kern = nodepool.kern local traverse = node.traverse local find_node_tail = node.tail or node.slide local tosequence = nodes.tosequence -- local function dimensions(parent,start,stop) -- stop = stop and stop.next -- if parent then -- if stop then -- return list_dimensions(parent.glue_set,parent.glue_sign,parent.glue_order,start,stop) -- else -- return list_dimensions(parent.glue_set,parent.glue_sign,parent.glue_order,start) -- end -- else -- if stop then -- return list_dimensions(start,stop) -- else -- return list_dimensions(start) -- end -- end -- end -- -- -- more compact local function dimensions(parent,start,stop) if parent then return list_dimensions(parent.glue_set,parent.glue_sign,parent.glue_order,start,stop and stop.next) else return list_dimensions(start,stop and stop.next) end end -- is pardir important at all? local function inject_range(head,first,last,reference,make,stack,parent,pardir,txtdir) local width, height, depth = 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 head == first then if trace_backend then report_area("head: %04i %s %s %s => w=%p, h=%p, d=%p, c=%s",reference,pardir or "---",txtdir or "----",tosequence(first,last,true),width,height,depth,resolved) end result.next = first first.prev = result return result, last else if trace_backend then report_area("middle: %04i %s %s => w=%p, h=%p, d=%p, c=%s",reference,pardir or "---",txtdir or "----",tosequence(first,last,true),width,height,depth,resolved) end local prev = first.prev if prev then result.next = first result.prev = prev prev.next = result first.prev = result else result.next = first first.prev = result end if first == head.next then head.next = result -- hm, weird end return head, last end else return head, last end end local function inject_list(id,current,reference,make,stack,pardir,txtdir) local width, height, depth, correction = current.width, current.height, current.depth, 0 local moveright = false local first = current.list 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 last.id == glue_code and last.subtype == rightskip_code then local prev = last.prev moveright = first.id == glue_code and first.subtype == leftskip_code if prev and prev.id == glue_code and prev.subtype == parfillskip_code then width = dimensions(current,first,prev.prev) -- maybe not current as we already take care of it else if moveright and first.writable then width = width - first.spec.stretch*current.glue_set * current.glue_sign end if last.writable then width = width - last.spec.stretch*current.glue_set * 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_backend then report_area("box: %04i %s %s: w=%p, h=%p, d=%p, c=%s",reference,pardir or "---",txtdir or "----",width,height,depth,resolved) end if not first then current.list = result elseif moveright then -- brr no prevs done -- result after first local n = first.next result.next = n first.next = result result.prev = first if n then n.prev = result end else -- first after result result.next = first first.prev = result 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 = current.id if id == hlist_code or id == vlist_code then local r = current[attribute] -- somehow reference is true so the following fails (second one not done) in -- test \goto{test}[page(2)] test \gotobox{test}[page(2)] -- so let's wait till this fails again -- if not reference and r and (not skip or r > skip) then -- > or ~= if r and (not skip or r > skip) then -- > or ~= inject_list(id,current,r,make,stack,pardir,txtdir) end if r then done[r] = (done[r] or 0) + 1 end local list = current.list if list then local _ current.list, _, pardir, txtdir = inject_areas(list,attribute,make,stack,done,r or skip or 0,current,pardir,txtdir) end if r then done[r] = done[r] - 1 end elseif id == whatsit_code then local subtype = current.subtype if subtype == localpar_code then pardir = current.dir elseif subtype == dir_code then txtdir = current.dir end elseif id == glue_code and current.subtype == leftskip_code then -- any glue at the left? -- else local r = 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 current.subtype == 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 = current.next 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 = current.id if id == hlist_code or id == vlist_code then local r = current[attribute] if r and not done[r] then done[r] = true inject_list(id,current,r,make,stack,pardir,txtdir) end local list = current.list if list then current.list = inject_area(list,attribute,make,stack,done,current,pardir,txtdir) end elseif id == whatsit_code then local subtype = current.subtype if subtype == localpar_code then pardir = current.dir elseif subtype == dir_code then txtdir = current.dir end else local r = 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 = current.next end end return head, true end -- tracing local nodepool = nodes.pool local new_rule = nodepool.rule local new_kern = nodepool.kern local set_attribute = node.set_attribute 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 colorize(width,height,depth,n,reference,what) 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) rule[a_colormodel] = 1 -- gray color model rule[a_color] = u_color rule[a_transparency] = u_transparency if width < 0 then local kern = new_kern(width) rule.width = -width kern.next = rule rule.prev = kern return kern else return rule end end -- references: local nodepool = nodes.pool local new_kern = nodepool.kern 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) 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] if ht then if height < ht then height = ht end if depth < dp then depth = dp end end local annot = nodeinjections.reference(width,height,depth,set) if annot then nofreferences = nofreferences + 1 local result, current if trace_references then local step = 65536 result = hpack_list(colorize(width,height-step,depth-step,2,reference,"reference")) -- step subtracted so that we can see seperate links result.width = 0 current = result end if current then current.next = annot else result = annot end references.registerpage(n) result = hpack_list(result,0) result.width, result.height, result.depth = 0, 0, 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 -- 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] if ht then if height < ht then height = ht end if depth < dp then depth = dp end end local result, current if trace_destinations then local step = 0 if width == 0 then step = 4*65536 width, height, depth = 5*step, 5*step, 0 end for n=1,#name do local rule = hpack_list(colorize(width,height,depth,3,reference,"destination")) rule.width = 0 if not result then result, current = rule, rule else current.next = rule rule.prev = current current = rule end width, height = width - step, height - step end end nofdestinations = nofdestinations + 1 for n=1,#name do local annot = nodeinjections.destination(width,height,depth,name[n],view) if annot then -- probably duplicate if not result then result = annot else current.next = annot annot.prev = current end current = find_node_tail(annot) end end if result then -- some internal error result = hpack_list(result,0) result.width, result.height, result.depth = 0, 0, 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 -- will move function references.mark(reference,h,d,view) return setdestination(tex.currentgrouplevel,h,d,reference,view) end function references.inject(prefix,reference,h,d,highlight,newwindow,layer) -- todo: use currentreference is possible 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, set.newwindow, set.layer = highlight, newwindow, layer setreference(h,d,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 commands.injectreference = references.inject commands.injectcurrentreference = references.injectcurrentset -- 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") end