node-syn.lmt /size: 23 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['node-syn'] = {
2    version   = 1.001,
3    comment   = "companion to node-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Because we have these fields in some node that are used by synctex, and because
10-- some users seem to like that feature, I decided to implement a variant that might
11-- work out better for ConTeXt. This is experimental code. I don't use it myself so
12-- it will take a while to mature. There will be some helpers that one can use in
13-- more complex situations like included xml files. Currently (somewhere else) we
14-- take care of valid files, that is: we prohibit access to files in the tree
15-- because we don't want users to mess up styles.
16--
17-- It is unclear how the output gets interpreted but by reverse engineering (and
18-- stripping) the file generated by generic synctex, I got there eventually. For
19-- instance, we only need to be able to go back to a place where text is entered,
20-- but still we need all that redundant box wrapping. Anyway, I was able to get a
21-- minimal output and cross my fingers that the parser used in editors is not
22-- changed in fundamental ways.
23--
24-- I only tested SumatraPDF with SciTE, for which one needs to configure in the
25-- viewer:
26--
27--   InverseSearchCmdLine = c:\data\system\scite\wscite\scite.exe "%f" "-goto:%l" $
28--
29-- In fact, a way more powerful implementation would have been not to add a library
30-- to a viewer, but letthe viewer call an external program:
31--
32--   InverseSearchCmdLine = mtxrun.exe --script synctex --edit --name="%f" --line="%l" $
33--
34-- which would (re)launch the editor in the right spot. That way we can really
35-- tune well to the macro package used and also avoid the fuzzy heuristics of
36-- the library.
37--
38-- Unfortunately syntex always removes the files at the end and not at the start
39-- (this happens in synctexterminate) so we need to work around that by using an
40-- intermediate file. This is no big deal in context (which has a runner) but
41-- definitely not nice.
42--
43-- The visualizer code is only needed for testing so we don't use fancy colors or
44-- provide more detail. After all we're only interested in rendered source text
45-- anyway. We try to play safe which sometimes means that we'd better no go
46-- somewhere than go someplace wrong.
47--
48-- A previous version had a mode for exporting boxes and such but I removed that
49-- as it made no sense. Also, collecting output in a table was not faster than
50-- directly piping to the file, probably because the amount is not that large. We
51-- keep some left-overs commented.
52--
53-- A significate reduction in file size can be realized when reusing the same
54-- values. Actually that triggered the current approach in ConTeXt. In the latest
55-- synctex parser vertical positions can be repeated by an "=" sign but for some
56-- reason only for that field. It's probably trivial to do that for all of "w h d v
57-- h" but it currently not the case so I'll delay that till all are supported. (We
58-- could benefit a lot from such a repetition scheme but not much from a "v" alone
59-- which -alas- indicates that synctex is still mostly a latex targeted story.)
60--
61-- It's kind of hard to fight the parser because it really wants to go to some file
62-- but maybe some day I can figure it out. Some untagged text (in the pdf) somehow
63-- gets seen as part of the last box. Anonymous content is simply not part of the
64-- concept. Using a dummy name doesn't help either as the editor gets a signal to
65-- open that dummy. Even an empty filename doesn't work.
66--
67-- We output really simple and compact code, like:
68--
69-- SyncTeX Version:1
70-- Input:1:e:/tmp/oeps.tex
71-- Input:2:c:/data/develop/context/sources/klein.tex
72-- Output:pdf
73-- Magnification:1000
74-- Unit:1
75-- X Offset:0
76-- Y Offset:0
77-- Content:
78-- !160
79-- {1
80-- h0,0:0,0,0,0,0
81-- v0,0:0,55380990:39158276,55380990,0
82-- h2,1:4661756,9176901:27969941,655360,327680
83-- h2,2:4661756,10125967:26048041,655360,327680
84-- h2,3:30962888,10125967:1668809,655360,327680
85-- h2,3:4661756,11075033:23142527,655360,327680
86-- h2,4:28046650,11075033:4585047,655360,327680
87-- h2,4:4661756,12024099:22913954,655360,327680
88-- h2,5:27908377,12024099:4723320,655360,327680
89-- h2,5:4661756,12973165:22918783,655360,327680
90-- h2,6:27884864,12973165:4746833,655360,327680
91-- h2,6:4661756,13922231:18320732,655360,327680
92-- ]
93-- !533
94-- }1
95-- Input:3:c:/data/develop/context/sources/ward.tex
96-- !57
97-- {2
98-- h0,0:0,0,0,0,0
99-- v0,0:0,55380990:39158276,55380990,0
100-- h3,1:4661756,9176901:18813145,655360,327680
101-- h3,2:23713999,9176901:8917698,655360,327680
102-- h3,2:4661756,10125967:10512978,655360,327680
103-- h3,3:15457206,10125967:17174491,655360,327680
104-- h3,3:4661756,11075033:3571223,655360,327680
105-- h3,4:8459505,11075033:19885281,655360,327680
106-- h3,5:28571312,11075033:4060385,655360,327680
107-- h3,5:4661756,12024099:15344870,655360,327680
108-- ]
109-- !441
110-- }2
111-- !8
112-- Postamble:
113-- Count:22
114-- !23
115-- Post scriptum:
116--
117-- But for some reason, when the pdf file has some extra content (like page numbers)
118-- the main document is consulted. Bah. It would be nice to have a mode for *only*
119-- looking at marked areas. It somehow works not but maybe depends on the parser.
120--
121-- Supporting reuseable objects makes not much sense as these are often graphics or
122-- ornamental. They can not have hyperlinks etc (at least not without some hackery
123-- which I'm not willing to do) so basically they are sort of useless for text.
124--
125-- Some generic (more clever code) has been removed as I don't see things change
126-- that much.
127
128local type, rawset, rawget = type, rawset, rawget
129local concat = table.concat
130local formatters = string.formatters
131local replacesuffix, suffixonly, nameonly, collapsepath = file.replacesuffix, file.suffix, file.nameonly, file.collapsepath
132local openfile, renamefile, removefile = io.open, os.rename, os.remove
133
134local report_system = logs.reporter("system")
135
136local tex                = tex
137local texget             = tex.get
138
139local nuts               = nodes.nuts
140
141local getid              = nuts.getid
142local getlist            = nuts.getlist
143local setlist            = nuts.setlist
144local getnext            = nuts.getnext
145local getwhd             = nuts.getwhd
146local getsubtype         = nuts.getsubtype
147
148local nodecodes          = nodes.nodecodes
149local kerncodes          = nodes.kerncodes
150
151local nextnode           = nuts.traversers.node
152
153local glyph_code         = nodecodes.glyph
154local disc_code          = nodecodes.disc
155local glue_code          = nodecodes.glue
156local penalty_code       = nodecodes.penalty
157local kern_code          = nodecodes.kern
158local hlist_code         = nodecodes.hlist
159local vlist_code         = nodecodes.vlist
160local fontkern_code      = kerncodes.fontkern
161
162local insertbefore       = nuts.insertbefore
163local insertafter        = nuts.insertafter
164
165local nodepool           = nuts.pool
166local new_latelua        = nodepool.latelua
167local new_rule           = nodepool.rule
168local new_outline_rule   = nodepool.outlinerule
169local new_kern           = nodepool.kern
170
171local getdimensions      = nuts.dimensions
172local getrangedimensions = nuts.rangedimensions
173
174local getinputfields      = nuts.getinputfields
175local forceinputstatefile = tex.forceinputstatefile
176local forceinputstateline = tex.forceinputstateline
177local getinputstateline   = tex.getinputstateline
178local setinputstatemode   = tex.setinputstatemode
179
180local foundintree        = resolvers.foundintree
181
182local getpagedimensions  = layouts.getpagedimensions
183
184local eol        <const> = "\010"
185
186----- f_glue             = formatters["g%i,%i:%i,%i\010"]
187----- f_glyph            = formatters["x%i,%i:%i,%i\010"]
188----- f_kern             = formatters["k%i,%i:%i,%i:%i\010"]
189----- f_rule             = formatters["r%i,%i:%i,%i:%i,%i,%i\010"]
190----- f_form             = formatters["f%i,%i,%i\010"]
191local z_hlist    <const> = "[0,0:0,0:0,0,0\010"
192local z_vlist    <const> = "(0,0:0,0:0,0,0\010"
193----- z_xform    <const> = "<0,0:0,0,0\010" -- or so
194local s_hlist    <const> = "]\010"
195local s_vlist    <const> = ")\010"
196----- s_xform    <const> = ">\010"
197local f_hlist_1          = formatters["h%i,%i:%i,%i:%i,%i,%i\010"]
198local f_hlist_2          = formatters["h%i,%i:%i,%s:%i,%i,%i\010"]
199local f_vlist_1          = formatters["v%i,%i:%i,%i:%i,%i,%i\010"]
200local f_vlist_2          = formatters["v%i,%i:%i,%s:%i,%i,%i\010"]
201
202local synctex            = luatex.synctex or { }
203luatex.synctex           = synctex
204
205local getpos ; getpos = function() getpos = job.positions.getpos return getpos() end
206
207-- status stuff
208
209local enabled = false
210local paused  = 0
211local used    = false
212local never   = false
213
214-- the file name stuff (called tags in synctex)
215
216local noftags            = 0
217local stnums             = { }
218local nofblocked         = 0
219local blockedfilenames   = { }
220local blockedsuffixes    = {
221    mkii = true,
222    mkiv = true,
223    mkvi = true,
224    mkxl = true,
225    mklx = true,
226    mkix = true,
227    mkxi = true,
228 -- lfg  = true,
229}
230
231local sttags = table.setmetatableindex(function(t,fullname)
232    local name = collapsepath(fullname)
233    if blockedsuffixes[suffixonly(name)] then
234        -- Just so that I don't get the ones on my development tree.
235        nofblocked = nofblocked + 1
236        return 0
237    elseif blockedfilenames[nameonly(name)] then
238        -- So we can block specific files.
239        nofblocked = nofblocked + 1
240        return 0
241    elseif foundintree(name) then
242        -- One shouldn't edit styles etc this way.
243        nofblocked = nofblocked + 1
244        return 0
245    else
246        noftags = noftags + 1
247        t[name] = noftags
248        if name ~= fullname then
249            t[fullname] = noftags
250        end
251        stnums[noftags] = name
252        return noftags
253    end
254end)
255
256function synctex.blockfilename(name)
257    blockedfilenames[nameonly(name)] = name
258end
259
260function synctex.setfilename(name,line)
261    if paused == 0 and name then
262        forceinputstatefile(sttags[name])
263        if line then
264            forceinputstateline(line)
265        end
266    end
267end
268
269function synctex.resetfilename()
270    if paused == 0 then
271        forceinputstatefile(0)
272        forceinputstateline(0)
273    end
274end
275
276do
277
278    local nesting = 0
279    local ignored = false
280
281    function synctex.pushline()
282        nesting = nesting + 1
283        if nesting == 1 then
284            local l = getinputstateline()
285            ignored = l and l > 0
286            if not ignored then
287                forceinputstateline(texget("inputlineno"))
288            end
289        end
290    end
291
292    function synctex.popline()
293        if nesting == 1 then
294            if not ignored then
295                forceinputstateline()
296                ignored = false
297            end
298        end
299        nesting = nesting - 1
300    end
301
302end
303
304-- the node stuff
305
306local filehandle = nil
307local nofsheets  = 0
308local nofobjects = 0
309local last       = 0
310local filesdone  = 0
311local tmpfile    = false
312local logfile    = false
313
314local function writeanchor()
315    local size = filehandle:seek("end")
316    filehandle:write("!",size-last,eol)
317    last = size
318end
319
320local function writefiles()
321    local total = #stnums
322    if filesdone < total then
323        for i=filesdone+1,total do
324            filehandle:write("Input:",i,":",stnums[i],eol)
325        end
326        filesdone = total
327    end
328end
329
330local function makenames()
331    logfile = replacesuffix(tex.jobname,"synctex")
332    tmpfile = replacesuffix(logfile,"syncctx")
333end
334
335local function flushpreamble()
336    makenames()
337    filehandle = openfile(tmpfile,"wb")
338    if filehandle then
339        filehandle:setvbuf("full",64*1024)
340        filehandle:write("SyncTeX Version:1",eol)
341        writefiles()
342        filehandle:write("Output:pdf",eol)
343        filehandle:write("Magnification:1000",eol)
344        filehandle:write("Unit:1",eol)
345        filehandle:write("X Offset:0",eol)
346        filehandle:write("Y Offset:0",eol)
347        filehandle:write("Content:",eol)
348        flushpreamble = function()
349            writefiles()
350            return filehandle
351        end
352    else
353        enabled = false
354    end
355    return filehandle
356end
357
358function synctex.wrapup()
359    if tmpfile then
360        renamefile(tmpfile,logfile)
361        tmpfile = nil
362    end
363end
364
365local function flushpostamble()
366    if not filehandle then
367        return
368    end
369    writeanchor()
370    filehandle:write("Postamble:",eol)
371    filehandle:write("Count:",nofobjects,eol)
372    writeanchor()
373    filehandle:write("Post scriptum:",eol)
374    filehandle:close()
375    enabled = false
376end
377
378-- local x_hlist  do
379--
380--     local function doaction_1(t,l,w,h,d)
381--         local pagewidth, pageheight = getpagedimensions()
382--         local x, y = getpos()
383--         filehandle:write(f_hlist_1(t,l,x,pageheight-y,w,h,d))
384--         nofobjects = nofobjects + 1
385--     end
386--
387--     local lasty = false
388--
389--     local function doaction_2(t,l,w,h,d)
390--         local pagewidth, pageheight = getpagedimensions()
391--         local x, y = getpos()
392--         y = pageheight - y
393--         filehandle:write(f_hlist_2(t,l,x,y == lasty and "=" or y,w,h,d))
394--         lasty = y
395--         nofobjects = nofobjects + 1
396--     end
397--
398--     local doaction = doaction_1
399--
400--     x_hlist = function(head,current,t,l,w,h,d)
401--         if filehandle then
402--             return insertbefore(head,current,new_latelua(function() doaction(t,l,w,h,d) end))
403--         else
404--             return head
405--         end
406--     end
407--
408--     directives.register("system.synctex.compression", function(v)
409--         doaction = tonumber(v) == 2 and doaction_2 or doaction_1
410--     end)
411--
412-- end
413
414-- local pagewidth, pageheight
415
416local x_hlist  do
417
418    local function doaction_1(data)
419        local pagewidth, pageheight = getpagedimensions()
420        local x, y = getpos()
421        filehandle:write(f_hlist_1(data[1],data[2],x,pageheight-y,data[3],data[4],data[5]))
422        nofobjects = nofobjects + 1
423    end
424
425    local lasty = false
426
427    local function doaction_2(data)
428        local pagewidth, pageheight = getpagedimensions()
429        local x, y = getpos()
430        y = pageheight - y
431        filehandle:write(f_hlist_2(t[1],data[2],x,y == lasty and "=" or y,data[3],data[3],data[5]))
432        lasty = y
433        nofobjects = nofobjects + 1
434    end
435
436    local doaction = doaction_1
437
438    x_hlist = function(head,current,t,l,w,h,d)
439        if filehandle then
440            return insertbefore(head,current,new_latelua { action = doaction, t, l, w, h, d })
441        else
442            return head
443        end
444    end
445
446    directives.register("system.synctex.compression", function(v)
447        doaction = tonumber(v) == 2 and doaction_2 or doaction_1
448    end)
449
450end
451
452-- color is already handled so no colors
453
454local collect      = nil
455local fulltrace    = false
456local trace        = false
457local height       = 10 * 65536
458local depth        =  5 * 65536
459local traceheight  =      32768
460local tracedepth   =      32768
461local outlinewidth = 65536 / 4
462
463trackers.register("system.synctex.visualize", function(v)
464    trace     = v
465    fulltrace = v == "real"
466end)
467
468local function inject(head,first,last,tag,line)
469    local w, h, d = getdimensions(first,getnext(last))
470    if h < height then
471        h = height
472    end
473    if d < depth then
474        d = depth
475    end
476    if trace then
477        if fulltrace then
478            head = insertbefore(head,first,new_outline_rule(w,h,d,outlinewidth))
479        else
480            head = insertbefore(head,first,new_rule(w,traceheight,tracedepth))
481        end
482        head = insertbefore(head,first,new_kern(-w))
483    end
484    head = x_hlist(head,first,tag,line,w,h,d)
485    return head
486end
487
488local function collect_min(head)
489    local current = head
490    while current do
491        local id = getid(current)
492        if id == glyph_code then
493            local first = current
494            local last  = current
495            local tag   = 0
496            local line  = 0
497            while true do
498                if id == glyph_code then
499                    local tc, lc = getinputfields(current)
500                    if tc and tc > 0 then
501                        tag  = tc
502                        line = lc
503                    end
504                    last = current
505                elseif id == disc_code or (id == kern_code and getsubtype(current) == fontkern_code) then
506                    last = current
507                else
508                    if tag > 0 then
509                        head = inject(head,first,last,tag,line)
510                    end
511                    break
512                end
513                current = getnext(current)
514                if current then
515                    id = getid(current)
516                else
517                    if tag > 0 then
518                        head = inject(head,first,last,tag,line)
519                    end
520                    return head
521                end
522            end
523        end
524        -- pick up (as id can have changed)
525        if id == hlist_code or id == vlist_code then
526            local list = getlist(current)
527            if list then
528                local l = collect(list)
529                if l ~= list then
530                    setlist(current,l)
531                end
532            end
533        end
534        current = getnext(current)
535    end
536    return head
537end
538
539local function inject(parent,head,first,last,tag,line)
540    local w, h, d = getrangedimensions(parent,first,getnext(last))
541    if h < height then
542        h = height
543    end
544    if d < depth then
545        d = depth
546    end
547    if trace then
548        if fulltrace then
549            head = insertbefore(head,first,new_outline_rule(w,h,d,outlinewidth))
550        else
551            head = insertbefore(head,first,new_rule(w,traceheight,tracedepth))
552        end
553        head = insertbefore(head,first,new_kern(-w))
554    end
555    head = x_hlist(head,first,tag,line,w,h,d)
556    return head
557end
558
559local function collect_max(head,parent)
560    local current = head
561    while current do
562        local id = getid(current)
563        if id == glyph_code then
564            local first = current
565            local last  = current
566            local tag   = 0
567            local line  = 0
568            while true do
569                if id == glyph_code then
570                    local tc, lc = getinputfields(current)
571                    if tc and tc > 0 then
572                        if tag > 0 and (tag ~= tc or line ~= lc) then
573                            head  = inject(parent,head,first,last,tag,line)
574                            first = current
575                        end
576                        tag  = tc
577                        line = lc
578                        last = current
579                    else
580                        if tag > 0 then
581                            head = inject(parent,head,first,last,tag,line)
582                            tag  = 0
583                        end
584                        first = nil
585                        last  = nil
586                    end
587                elseif id == disc_code then
588                    if not first then
589                        first = current
590                    end
591                    last = current
592                elseif id == kern_code and getsubtype(current) == fontkern_code then
593                    if first then
594                        last = current
595                    end
596                elseif id == glue_code then
597                elseif id == penalty_code then
598                    -- go on (and be nice for math)
599                else
600                    if tag > 0 then
601                        head = inject(parent,head,first,last,tag,line)
602                        tag  = 0
603                    end
604                    break
605                end
606                current = getnext(current)
607                if current then
608                    id = getid(current)
609                else
610                    if tag > 0 then
611                        head = inject(parent,head,first,last,tag,line)
612                    end
613                    return head
614                end
615            end
616        end
617        -- pick up (as id can have changed)
618        if id == hlist_code or id == vlist_code then
619            local list = getlist(current)
620            if list then
621                local l = collect(list,current)
622                if l and l ~= list then
623                    setlist(current,l)
624                end
625            end
626        end
627        current = getnext(current)
628    end
629    return head
630end
631
632collect = collect_max
633
634function synctex.collect(head,where)
635    if enabled and where ~= "object" then
636        return collect(head,head)
637    else
638        return head
639    end
640end
641
642-- also no solution for bad first file resolving in sumatra
643
644function synctex.start()
645    if enabled then
646        nofsheets = nofsheets + 1 -- could be realpageno
647        if flushpreamble() then
648            writeanchor()
649            filehandle:write("{",nofsheets,eol)
650            -- this seems to work:
651            local pagewidth, pageheight = getpagedimensions()
652-- pagewidth, pageheight = getpagedimensions()
653            filehandle:write(z_hlist)
654            filehandle:write(f_vlist_1(0,0,0,pageheight,pagewidth,pageheight,0))
655        end
656    end
657end
658
659function synctex.stop()
660    if enabled then
661     -- filehandle:write(s_vlist,s_hlist)
662        filehandle:write(s_hlist)
663        writeanchor()
664        filehandle:write("}",nofsheets,eol)
665        nofobjects = nofobjects + 2
666    end
667end
668
669local enablers  = { }
670local disablers = { }
671
672function synctex.registerenabler(f)
673    enablers[#enablers+1] = f
674end
675
676function synctex.registerdisabler(f)
677    disablers[#disablers+1] = f
678end
679
680function synctex.enable()
681    if not never and not enabled then
682        enabled = true
683        setinputstatemode(3) -- we want details
684        if not used then
685            nodes.tasks.enableaction("shipouts","luatex.synctex.collect")
686            report_system("synctex functionality is enabled, expect 5-10 pct runtime overhead!")
687            used = true
688        end
689        for i=1,#enablers do
690            enablers[i](true)
691        end
692        -- we have a different trigger moment in lmtx
693        flushpreamble()
694    end
695end
696
697function synctex.disable()
698    if enabled then
699        setinputstatemode(0)
700        report_system("synctex functionality is disabled!")
701        enabled = false
702        for i=1,#disablers do
703            disablers[i](false)
704        end
705    end
706end
707
708function synctex.finish()
709    if enabled then
710        flushpostamble()
711    else
712        makenames()
713        removefile(logfile)
714        removefile(tmpfile)
715    end
716end
717
718local filename = nil
719
720function synctex.pause()
721    paused = paused + 1
722    if enabled and paused == 1 then
723        setinputstatemode(0)
724    end
725end
726
727function synctex.resume()
728    if enabled and paused == 1 then
729        setinputstatemode(3)
730    end
731    paused = paused - 1
732end
733
734-- not the best place
735
736luatex.registerstopactions(synctex.finish)
737
738statistics.register("synctex tracing",function()
739    if used then
740        return string.format("%i referenced files, %i files ignored, %i objects flushed, logfile: %s",
741            noftags,nofblocked,nofobjects,logfile)
742    end
743end)
744
745local implement = interfaces.implement
746local variables = interfaces.variables
747
748function synctex.setup(t)
749    if t.state == variables.never then
750        synctex.disable() -- just in case
751        never = true
752        return
753    end
754    if t.method == variables.max then
755        collect = collect_max
756    else
757        collect = collect_min
758    end
759    if t.state == variables.start then
760        synctex.enable()
761    else
762        synctex.disable()
763    end
764end
765
766implement {
767    name      = "synctexblockfilename",
768    arguments = "string",
769    protected = true,
770    public    = true,
771    actions   = synctex.blockfilename,
772}
773
774implement {
775    name      = "synctexsetfilename",
776    arguments = "string",
777    protected = true,
778    public    = true,
779    actions   = synctex.setfilename,
780}
781
782implement {
783    name      = "synctexresetfilename",
784    protected = true,
785    public    = true,
786    actions   = synctex.resetfilename,
787}
788
789implement {
790    name      = "setupsynctex",
791    actions   = synctex.setup,
792    arguments = {
793        {
794            { "state" },
795            { "method" },
796        },
797    },
798}
799
800implement {
801    name    = "synctexpause",
802    actions = synctex.pause,
803}
804
805implement {
806    name    = "synctexresume",
807    actions = synctex.resume,
808}
809
810implement {
811    name    = "synctexpushline",
812    actions = synctex.pushline,
813}
814
815implement {
816    name    = "synctexpopline",
817    actions = synctex.popline,
818}
819
820implement {
821    name    = "synctexdisable",
822    actions = synctex.disable,
823}
824