if not modules then modules = { } end modules ['node-syn'] = { version = 1.001, comment = "companion to node-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- Because we have these fields in some nodes that are used by synctex, and because -- some users seem to like that feature, I decided to implement a variant that might -- work out better for ConTeXt. This is experimental code. I don't use it myself so -- it will take a while to mature. There will be some helpers that one can use in -- more complex situations like included xml files. Currently (somewhere else) we -- take care of valid files, that is: we prohibit access to files in the tree -- because we don't want users to mess up styles. -- -- It is unclear how the output gets interpreted but by reverse engineering (and -- stripping) the file generated by generic synctex, I got there eventually. For -- instance, we only need to be able to go back to a place where text is entered, -- but still we need all that redundant box wrapping. Anyway, I was able to get a -- minimal output and cross my fingers that the parser used in editors is not -- changed in fundamental ways. -- -- I only tested SumatraPDF with SciTE, for which one needs to configure in the -- viewer: -- -- InverseSearchCmdLine = c:\data\system\scite\wscite\scite.exe "%f" "-goto:%l" $ -- -- In fact, a way more powerful implementation would have been not to add a library -- to a viewer, but letthe viewer call an external program: -- -- InverseSearchCmdLine = mtxrun.exe --script synctex --edit --name="%f" --line="%l" $ -- -- which would (re)launch the editor in the right spot. That way we can really -- tune well to the macro package used and also avoid the fuzzy heuristics of -- the library. -- -- Unfortunately syntex always removes the files at the end and not at the start -- (this happens in synctexterminate) so we need to work around that by using an -- intermediate file. This is no big deal in context (which has a runner) but -- definitely not nice. This is no longer an issue in LMTX because there we don't -- have the library interfering with files. -- -- The visualizer code is only needed for testing so we don't use fancy colors or -- provide more detail. After all we're only interested in rendered source text -- anyway. We try to play safe which sometimes means that we'd better no go -- somewhere than go someplace wrong. -- -- A previous version had a mode for exporting boxes and such but I removed that -- as it made no sense. Also, collecting output in a table was not faster than -- directly piping to the file, probably because the amount is not that large. We -- keep some left-overs commented. -- -- A significant reduction in file size can be realized when reusing the same -- values. Actually that triggered the current approach in ConTeXt. In the latest -- synctex parser vertical positions can be repeated by an "=" sign but for some -- reason only for that field. It's probably trivial to do that for all of "w h d v -- h" but it currently not the case so I'll delay that till all are supported. (We -- could benefit a lot from such a repetition scheme but not much from a "v" alone -- which -alas- indicates that synctex is still mostly a latex targeted story.) -- -- It's kind of hard to fight the parser because it really wants to go to some file -- but maybe some day I can figure it out. Some untagged text (in the pdf) somehow -- gets seen as part of the last box. Anonymous content is simply not part of the -- concept. Using a dummy name doesn't help either as the editor gets a signal to -- open that dummy. Even an empty filename doesn't work. -- -- We output really simple and compact code, like: -- -- SyncTeX Version:1 -- Input:1:e:/tmp/oeps.tex -- Input:2:c:/data/develop/context/sources/klein.tex -- Output:pdf -- Magnification:1000 -- Unit:1 -- X Offset:0 -- Y Offset:0 -- Content: -- !160 -- {1 -- h0,0:0,0,0,0,0 -- v0,0:0,55380990:39158276,55380990,0 -- h2,1:4661756,9176901:27969941,655360,327680 -- h2,2:4661756,10125967:26048041,655360,327680 -- h2,3:30962888,10125967:1668809,655360,327680 -- h2,3:4661756,11075033:23142527,655360,327680 -- h2,4:28046650,11075033:4585047,655360,327680 -- h2,4:4661756,12024099:22913954,655360,327680 -- h2,5:27908377,12024099:4723320,655360,327680 -- h2,5:4661756,12973165:22918783,655360,327680 -- h2,6:27884864,12973165:4746833,655360,327680 -- h2,6:4661756,13922231:18320732,655360,327680 -- ] -- !533 -- }1 -- Input:3:c:/data/develop/context/sources/ward.tex -- !57 -- {2 -- h0,0:0,0,0,0,0 -- v0,0:0,55380990:39158276,55380990,0 -- h3,1:4661756,9176901:18813145,655360,327680 -- h3,2:23713999,9176901:8917698,655360,327680 -- h3,2:4661756,10125967:10512978,655360,327680 -- h3,3:15457206,10125967:17174491,655360,327680 -- h3,3:4661756,11075033:3571223,655360,327680 -- h3,4:8459505,11075033:19885281,655360,327680 -- h3,5:28571312,11075033:4060385,655360,327680 -- h3,5:4661756,12024099:15344870,655360,327680 -- ] -- !441 -- }2 -- !8 -- Postamble: -- Count:22 -- !23 -- Post scriptum: -- -- But for some reason, when the pdf file has some extra content (like page numbers) -- the main document is consulted. Bah. It would be nice to have a mode for *only* -- looking at marked areas. It somehow works not but maybe depends on the parser. -- -- Supporting reuseable objects makes not much sense as these are often graphics or -- ornamental. They can not have hyperlinks etc (at least not without some hackery -- which I'm not willing to do) so basically they are sort of useless for text. -- -- Some generic (more clever code) has been removed as I don't see things change -- that much. However, we do have a problem. Files that work okay in sumatra, and -- on linux in zathura pdf, don't work well in for instance okular. When Mikael -- Sundqvist installed SciTE we tried to get it working with Okular and failed to -- do so. It took us a while to figure out the reason: -- -- - There has been a change in library. In Sumatra there is built in code for -- an older format and it uses an old version of the library for the new format. -- - In Zathura an old version of the library is used. -- - Okular seems to use a newer version. -- -- So, then we tested how a simple latex file performed ane eventually we figured -- out a work-around. -- -- The parser hooked into the viewer is way too complex for what context needs. We -- just need areas but there are heuristics, probably geared for how latex outputs -- pages. It also looks like the newer library ignores (what it calls) void boxes -- which is what we use. However, when we put a fake rule in there we can get it -- working; it's an ugly hack but hopefully one that will work from now on. What we -- did notice is that an empty box with dimensions didn't work in latex either. Even -- stranger was that the file produced by lualatex did have the right line numbers -- while the file made by pdflatex didn't, so actually we assume support for latex -- is also fragile. -- -- We still hope that some day editors will delegate resolving the coordinates and -- launching the editor to an external program (script). Some viewers communicate -- via a so called dbus already so wel'll see. If that (ever) happens we can drop -- the current synctex output for a more efficient one. -- This version has some details stripped because they are irrelevant in the -- perspective of \LUAMETATEX. -- -- Compression doesn't work out well because there are old and new synctex -- versions in viewers and it makes no sense to accomodate this and guessing if -- it works. It's also only related to vertical coordinates so the savings are -- little. local type, rawset = type, rawset local concat = table.concat local formatters = string.formatters local replacesuffix, suffixonly, nameonly, collapsepath = file.replacesuffix, file.suffix, file.nameonly, file.collapsepath local openfile, removefile = io.open, os.remove local report_system = logs.reporter("system") local tex = tex local texget = tex.get local nuts = nodes.nuts local getid = nuts.getid local getlist = nuts.getlist local setlist = nuts.setlist local getnext = nuts.getnext local getwhd = nuts.getwhd local getsubtype = nuts.getsubtype local nodecodes = nodes.nodecodes local kerncodes = nodes.kerncodes local glyph_code = nodecodes.glyph local disc_code = nodecodes.disc local glue_code = nodecodes.glue local penalty_code = nodecodes.penalty local kern_code = nodecodes.kern local hlist_code = nodecodes.hlist local vlist_code = nodecodes.vlist local fontkern_code = kerncodes.fontkern local insertbefore = nuts.insertbefore local insertafter = nuts.insertafter local nextnode = nuts.traversers.node local nodepool = nuts.pool local new_latelua = nodepool.latelua local new_rule = nodepool.rule local new_outline_rule = nodepool.outlinerule local new_kern = nodepool.kern local getdimensions = nuts.dimensions local getrangedimensions = nuts.rangedimensions local getinputfields = nuts.getinputfields local forceinputstatefile = tex.forceinputstatefile local forceinputstateline = tex.forceinputstateline local getinputstateline = tex.getinputstateline local setinputstatemode = tex.setinputstatemode local foundintree = resolvers.foundintree local getpagedimensions = layouts.getpagedimensions local eol = "\010" local z_hlist = "[0,0:0,0:0,0,0\010" local s_hlist = "]\010" local f_hvoid = formatters["h%i,%i:%i,%i:%i,%i,%i\010"] local f_hlist = formatters["(%i,%i:%i,%i:%i,%i,%i\010"] local s_hlist = ")\010" local f_rule = formatters["r%i,%i:%i,%s:%i,%i,%i\010"] local f_plist = formatters["[%i,%i:%i,%i:%i,%i,%i\010"] local s_plist = "]\010" local synctex = luatex.synctex or { } luatex.synctex = synctex local getpos ; getpos = function() getpos = job.positions.getpos return getpos() end -- status stuff local enabled = false local userule = false local paused = 0 local used = false local never = false -- the file name stuff (called tags in synctex) local noftags = 0 local stnums = { } local nofblocked = 0 local blockedfilenames = { } local blockedsuffixes = { mkii = true, mkiv = true, mkvi = true, mkxl = true, mklx = true, mkix = true, mkxi = true, -- lfg = true, } local sttags = table.setmetatableindex(function(t,fullname) local name = collapsepath(fullname) if blockedsuffixes[suffixonly(name)] then -- Just so that I don't get the ones on my development tree. nofblocked = nofblocked + 1 return 0 elseif blockedfilenames[nameonly(name)] then -- So we can block specific files. nofblocked = nofblocked + 1 return 0 elseif foundintree(name) then -- One shouldn't edit styles etc this way. nofblocked = nofblocked + 1 return 0 else noftags = noftags + 1 t[name] = noftags if name ~= fullname then t[fullname] = noftags end stnums[noftags] = name return noftags end end) function synctex.blockfilename(name) blockedfilenames[nameonly(name)] = name end function synctex.setfilename(name,line) if paused == 0 and name then forceinputstatefile(sttags[name]) if line then forceinputstateline(line) end end end function synctex.resetfilename() if paused == 0 then forceinputstatefile(0) forceinputstateline(0) end end do local nesting = 0 local ignored = false function synctex.pushline() nesting = nesting + 1 if nesting == 1 then local l = getinputstateline() ignored = l and l > 0 if not ignored then forceinputstateline(texget("inputlineno")) end end end function synctex.popline() if nesting == 1 then if not ignored then forceinputstateline() ignored = false end end nesting = nesting - 1 end end -- the node stuff local filehandle = nil local nofsheets = 0 local nofobjects = 0 local last = 0 local filesdone = 0 local sncfile = false local function writeanchor() local size = filehandle:seek("end") filehandle:write("!",size-last,eol) last = size end local function writefiles() local total = #stnums if filesdone < total then for i=filesdone+1,total do filehandle:write("Input:",i,":",stnums[i],eol) end filesdone = total end end local function makenames() sncfile = replacesuffix(tex.jobname,"synctex") end local function flushpreamble() makenames() filehandle = openfile(sncfile,"wb") if filehandle then filehandle:setvbuf("full",64*1024) filehandle:write("SyncTeX Version:1",eol) writefiles() filehandle:write("Output:pdf",eol) filehandle:write("Magnification:1000",eol) filehandle:write("Unit:1",eol) filehandle:write("X Offset:0",eol) filehandle:write("Y Offset:0",eol) filehandle:write("Content:",eol) flushpreamble = function() writefiles() return filehandle end else enabled = false end return filehandle end function synctex.wrapup() sncfile = nil end local function flushpostamble() if not filehandle then return end writeanchor() filehandle:write("Postamble:",eol) filehandle:write("Count:",nofobjects,eol) writeanchor() filehandle:write("Post scriptum:",eol) filehandle:close() enabled = false end local x_hlist do local function doaction(data) local pagewidth, pageheight = getpagedimensions() -- we could save some by setting it per page local x, y = getpos() local t = data[1] local l = data[2] local w = data[3] local h = data[4] local d = data[5] y = pageheight - y if userule then -- This cheat works in viewers that use the newer library: filehandle:write(f_hlist(t,l,x,y,0,0,0)) -- w,h,d)) filehandle:write(f_rule(t,l,x,y,w,h,d)) filehandle:write(s_hlist) else -- This works in viewers that use the older library: filehandle:write(f_hvoid(t,l,x,y,w,h,d)) end nofobjects = nofobjects + 1 end x_hlist = function(head,current,t,l,w,h,d) if filehandle then return insertbefore(head,current,new_latelua { action = doaction, t, l, w, h, d }) else return head end end end -- color is already handled so no colors local collect = nil local fulltrace = false local trace = false local height = 10 * 65536 local depth = 5 * 65536 local traceheight = 32768 local tracedepth = 32768 local outlinewidth = 65536 / 4 trackers.register("system.synctex.visualize", function(v) trace = v fulltrace = v == "real" end) local collect_min do local function inject(head,first,last,tag,line) local w, h, d = getdimensions(first,getnext(last)) if h < height then h = height end if d < depth then d = depth end if trace then if fulltrace then head = insertbefore(head,first,new_outline_rule(w,h,d,outlinewidth)) else head = insertbefore(head,first,new_rule(w,traceheight,tracedepth)) end head = insertbefore(head,first,new_kern(-w)) end head = x_hlist(head,first,tag,line,w,h,d) return head end collect_min = function(head) local current = head while current do local id = getid(current) if id == glyph_code then local first = current local last = current local tag = 0 local line = 0 while true do if id == glyph_code then local tc, lc = getinputfields(current) if tc and tc > 0 then tag = tc line = lc end last = current elseif id == disc_code or (id == kern_code and getsubtype(current) == fontkern_code) then last = current else if tag > 0 then head = inject(head,first,last,tag,line) end break end current = getnext(current) if current then id = getid(current) else if tag > 0 then head = inject(head,first,last,tag,line) end return head end end end -- pick up (as id can have changed) if id == hlist_code or id == vlist_code then local list = getlist(current) if list then local l = collect(list) if l ~= list then setlist(current,l) end end end current = getnext(current) end return head end end local collect_max do local function inject(parent,head,first,last,tag,line) local w, h, d = getrangedimensions(parent,first,getnext(last)) if h < height then h = height end if d < depth then d = depth end if trace then if fulltrace then head = insertbefore(head,first,new_outline_rule(w,h,d,outlinewidth)) else head = insertbefore(head,first,new_rule(w,traceheight,tracedepth)) end head = insertbefore(head,first,new_kern(-w)) end head = x_hlist(head,first,tag,line,w,h,d) return head end collect_max = function(head,parent) local current = head while current do local id = getid(current) if id == glyph_code then local first = current local last = current local tag = 0 local line = 0 while true do if id == glyph_code then local tc, lc = getinputfields(current) if tc and tc > 0 then if tag > 0 and (tag ~= tc or line ~= lc) then head = inject(parent,head,first,last,tag,line) first = current end tag = tc line = lc last = current else if tag > 0 then head = inject(parent,head,first,last,tag,line) tag = 0 end first = nil last = nil end elseif id == disc_code then if not first then first = current end last = current elseif id == kern_code and getsubtype(current) == fontkern_code then if first then last = current end elseif id == glue_code then elseif id == penalty_code then -- go on (and be nice for math) else if tag > 0 then head = inject(parent,head,first,last,tag,line) tag = 0 end break end current = getnext(current) if current then id = getid(current) else if tag > 0 then head = inject(parent,head,first,last,tag,line) end return head end end end -- pick up (as id can have changed) if id == hlist_code or id == vlist_code then local list = getlist(current) if list then local l = collect(list,current) if l and l ~= list then setlist(current,l) end end end current = getnext(current) end return head end end collect = collect_max function synctex.collect(head,where) if enabled and where ~= "object" then return collect(head,head) else return head end end -- also no solution for bad first file resolving in sumatra function synctex.start() if enabled then nofsheets = nofsheets + 1 -- could be realpageno if flushpreamble() then writeanchor() filehandle:write("{",nofsheets,eol) -- this seems to work: -- local pagewidth, pageheight = getpagedimensions() filehandle:write(f_plist(1,0,0,0,0,0,0)) end end end function synctex.stop() if enabled then filehandle:write(s_plist) writeanchor() filehandle:write("}",nofsheets,eol) nofobjects = nofobjects + 2 end end local enablers = { } local disablers = { } function synctex.registerenabler(f) enablers[#enablers+1] = f end function synctex.registerdisabler(f) disablers[#disablers+1] = f end function synctex.enable(use_rule) if tex.getinteraction() ~= tex.interactioncodes.errorstop then report_system("synctex functionality is only enabled in error stop mode") elseif not never and not enabled then enabled = true userule = use_rule setinputstatemode(3) -- we want details if not used then nodes.tasks.enableaction("shipouts","luatex.synctex.collect") report_system("synctex functionality is enabled, expect 5-10 pct runtime overhead!") used = true end for i=1,#enablers do enablers[i](true) end -- we have a different trigger moment in lmtx flushpreamble() end end function synctex.disable() if enabled then setinputstatemode(0) report_system("synctex functionality is disabled!") enabled = false for i=1,#disablers do disablers[i](false) end end end function synctex.finish() if enabled then flushpostamble() else makenames() removefile(sncfile) end end local filename = nil function synctex.pause() paused = paused + 1 if enabled and paused == 1 then setinputstatemode(0) end end function synctex.resume() if enabled and paused == 1 then setinputstatemode(3) end paused = paused - 1 end -- not the best place luatex.registerstopactions(synctex.finish) statistics.register("synctex tracing",function() if used then return string.format("%i referenced files, %i files ignored, %i objects flushed, logfile: %s", noftags,nofblocked,nofobjects,sncfile) end end) local implement = interfaces.implement local variables = interfaces.variables function synctex.setup(t) if t.state == variables.never then synctex.disable() -- just in case never = true return end if t.method == variables.max then collect = collect_max else collect = collect_min end if t.state == variables.start then synctex.enable(false) elseif t.state == variables["repeat"] then synctex.enable(true) else synctex.disable() end end implement { name = "synctexblockfilename", arguments = "string", protected = true, public = true, actions = synctex.blockfilename, } implement { name = "synctexsetfilename", arguments = "string", protected = true, public = true, actions = synctex.setfilename, } implement { name = "synctexresetfilename", protected = true, public = true, actions = synctex.resetfilename, } implement { name = "setupsynctex", actions = synctex.setup, arguments = { { { "state" }, { "method" }, }, }, } implement { name = "synctexpause", actions = synctex.pause, } implement { name = "synctexresume", actions = synctex.resume, } implement { name = "synctexpushline", actions = synctex.pushline, } implement { name = "synctexpopline", actions = synctex.popline, } implement { name = "synctexdisable", actions = synctex.disable, }