node-syn.lua /size: 18 Kb    last modification: 2025-02-21 11:03
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-- See node-syn.lmt for some comments. Because it's either unsupported and/or due
10-- to the mix of old and new libraries used in viewers the compressed (share same y
11-- coordinates) code has been removed. It doesn't save much anyway. I also removed
12-- the g, x, k, r, f specific code. The repeat mode has been added here too, which
13-- can work better with some versions of the libraries used in viewers.
14--
15-- Unfortunately there is still no way to signal that we don't want synctex to open
16-- a file.
17
18local type, rawset = type, rawset
19local concat = table.concat
20local formatters = string.formatters
21local replacesuffix, suffixonly, nameonly, collapsepath = file.replacesuffix, file.suffix, file.nameonly, file.collapsepath
22local openfile, renamefile, removefile = io.open, os.rename, os.remove
23
24local report_system = logs.reporter("system")
25
26local tex                 = tex
27local texget              = tex.get
28
29local nuts                = nodes.nuts
30
31local getid               = nuts.getid
32local getlist             = nuts.getlist
33local setlist             = nuts.setlist
34local getnext             = nuts.getnext
35local getwhd              = nuts.getwhd
36local getsubtype          = nuts.getsubtype
37
38local nodecodes           = nodes.nodecodes
39local kerncodes           = nodes.kerncodes
40
41local glyph_code          = nodecodes.glyph
42local disc_code           = nodecodes.disc
43local glue_code           = nodecodes.glue
44local penalty_code        = nodecodes.penalty
45local kern_code           = nodecodes.kern
46local hlist_code          = nodecodes.hlist
47local vlist_code          = nodecodes.vlist
48local fontkern_code       = kerncodes.fontkern
49
50local insertbefore        = nuts.insertbefore
51local insertafter         = nuts.insertafter
52
53local nodepool            = nuts.pool
54local new_latelua         = nodepool.latelua
55local new_rule            = nodepool.rule
56local new_kern            = nodepool.kern
57
58local getdimensions       = nuts.dimensions
59local getrangedimensions  = nuts.rangedimensions
60
61local getinputfields      = nuts.getsynctexfields
62local forceinputstatefile = tex.forcesynctextag   or tex.force_synctex_tag
63local forceinputstateline = tex.forcesynctexline  or tex.force_synctex_line
64local getinputstateline   = tex.getsynctexline    or tex.get_synctex_line
65local setinputstatemode   = tex.setsynctexmode    or tex.set_synctex_mode
66
67local foundintree         = resolvers.foundintree
68
69local getpagedimensions   = nil --defined later
70
71local eol                 = "\010"
72local z_hlist             = "[0,0:0,0:0,0,0\010"
73local s_hlist             = "]\010"
74local f_hvoid             = formatters["h%i,%i:%i,%i:%i,%i,%i\010"]
75local f_hlist             = formatters["(%i,%i:%i,%i:%i,%i,%i\010"]
76local s_hlist             = ")\010"
77local f_rule              = formatters["r%i,%i:%i,%s:%i,%i,%i\010"]
78local f_plist             = formatters["[%i,%i:%i,%i:%i,%i,%i\010"]
79local s_plist             = "]\010"
80
81local synctex             = luatex.synctex or { }
82luatex.synctex            = synctex
83
84local getpos ; getpos = function() getpos = job.positions.getpos return getpos() end
85
86-- status stuff
87
88local enabled = false
89local userule = false
90local paused  = 0
91local used    = false
92local never   = false
93
94-- get rid of overhead in mkiv
95
96tex.set_synctex_no_files(1)
97
98local noftags            = 0
99local stnums             = { }
100local nofblocked         = 0
101local blockedfilenames   = { }
102local blockedsuffixes    = {
103    mkii = true,
104    mkiv = true,
105    mkvi = true,
106    mkxl = true,
107    mklx = true,
108    mkix = true,
109    mkxi = true,
110 -- lfg  = true,
111}
112
113local sttags = table.setmetatableindex(function(t,fullname)
114    local name = collapsepath(fullname)
115    if blockedsuffixes[suffixonly(name)] then
116        -- Just so that I don't get the ones on my development tree.
117        nofblocked = nofblocked + 1
118        return 0
119    elseif blockedfilenames[nameonly(name)] then
120        -- So we can block specific files.
121        nofblocked = nofblocked + 1
122        return 0
123    elseif foundintree(name) then
124        -- One shouldn't edit styles etc this way.
125        nofblocked = nofblocked + 1
126        return 0
127    else
128        noftags = noftags + 1
129        t[name] = noftags
130        if name ~= fullname then
131            t[fullname] = noftags
132        end
133        stnums[noftags] = name
134        return noftags
135    end
136end)
137
138function synctex.blockfilename(name)
139    blockedfilenames[nameonly(name)] = name
140end
141
142function synctex.setfilename(name,line)
143    if paused == 0 and name then
144        forceinputstatefile(sttags[name])
145        if line then
146            forceinputstateline(line)
147        end
148    end
149end
150
151function synctex.resetfilename()
152    if paused == 0 then
153        forceinputstatefile(0)
154        forceinputstateline(0)
155    end
156end
157
158do
159
160    local nesting = 0
161    local ignored = false
162
163    function synctex.pushline()
164        nesting = nesting + 1
165        if nesting == 1 then
166            local l = getinputstateline()
167            ignored = l and l > 0
168            if not ignored then
169                forceinputstateline(texget("inputlineno"))
170            end
171        end
172    end
173
174    function synctex.popline()
175        if nesting == 1 then
176            if not ignored then
177                forceinputstateline()
178                ignored = false
179            end
180        end
181        nesting = nesting - 1
182    end
183
184end
185
186-- the node stuff
187
188local filehandle = nil
189local nofsheets  = 0
190local nofobjects = 0
191local last       = 0
192local filesdone  = 0
193local sncfile    = false
194
195local function writeanchor()
196    local size = filehandle:seek("end")
197    filehandle:write("!",size-last,eol)
198    last = size
199end
200
201local function writefiles()
202    local total = #stnums
203    if filesdone < total then
204        for i=filesdone+1,total do
205            filehandle:write("Input:",i,":",stnums[i],eol)
206        end
207        filesdone = total
208    end
209end
210
211local function makenames()
212    sncfile = replacesuffix(tex.jobname,"synctex")
213end
214
215local function flushpreamble()
216    makenames()
217    filehandle = openfile(sncfile,"wb")
218    if filehandle then
219        filehandle:write("SyncTeX Version:1",eol)
220        writefiles()
221        filehandle:write("Output:pdf",eol)
222        filehandle:write("Magnification:1000",eol)
223        filehandle:write("Unit:1",eol)
224        filehandle:write("X Offset:0",eol)
225        filehandle:write("Y Offset:0",eol)
226        filehandle:write("Content:",eol)
227        flushpreamble = function()
228            writefiles()
229            return filehandle
230        end
231    else
232        enabled = false
233    end
234    return filehandle
235end
236
237function synctex.wrapup()
238    sncfile = nil
239end
240
241local function flushpostamble()
242    if not filehandle then
243        return
244    end
245    writeanchor()
246    filehandle:write("Postamble:",eol)
247    filehandle:write("Count:",nofobjects,eol)
248    writeanchor()
249    filehandle:write("Post scriptum:",eol)
250    filehandle:close()
251    enabled = false
252end
253
254getpagedimensions = function()
255    getpagedimensions = backends.codeinjections.getpagedimensions
256    return getpagedimensions()
257end
258
259local x_hlist  do
260
261    local function doaction(t,l,w,h,d)
262        local pagewidth, pageheight = getpagedimensions() -- we could save some by setting it per page
263        local x, y = getpos()
264        y = pageheight - y
265        if userule then
266            -- This cheat works in viewers that use the newer library:
267            filehandle:write(f_hlist(t,l,x,y,0,0,0)) -- w,h,d))
268            filehandle:write(f_rule(t,l,x,y,w,h,d))
269            filehandle:write(s_hlist)
270        else
271            -- This works in viewers that use the older library:
272            filehandle:write(f_hvoid(t,l,x,y,w,h,d))
273        end
274        nofobjects = nofobjects + 1
275    end
276
277    x_hlist = function(head,current,t,l,w,h,d)
278        if filehandle then
279            return insertbefore(head,current,new_latelua(function() doaction(t,l,w,h,d) end))
280        else
281            return head
282        end
283    end
284
285end
286
287-- color is already handled so no colors
288
289local collect      = nil
290local fulltrace    = false
291local trace        = false
292local height       = 10 * 65536
293local depth        =  5 * 65536
294local traceheight  =      32768
295local tracedepth   =      32768
296
297trackers.register("system.synctex.visualize", function(v)
298    trace     = v
299    fulltrace = v == "real"
300end)
301
302local collect_min  do
303
304    local function inject(head,first,last,tag,line)
305        local w, h, d = getdimensions(first,getnext(last))
306        if h < height then
307            h = height
308        end
309        if d < depth then
310            d = depth
311        end
312        if trace then
313            head = insertbefore(head,first,new_rule(w,fulltrace and h or traceheight,fulltrace and d or tracedepth))
314            head = insertbefore(head,first,new_kern(-w))
315        end
316        head = x_hlist(head,first,tag,line,w,h,d)
317        return head
318    end
319
320    collect_min = function(head)
321        local current = head
322        while current do
323            local id = getid(current)
324            if id == glyph_code then
325                local first = current
326                local last  = current
327                local tag   = 0
328                local line  = 0
329                while true do
330                    if id == glyph_code then
331                        local tc, lc = getinputfields(current)
332                        if tc and tc > 0 then
333                            tag  = tc
334                            line = lc
335                        end
336                        last = current
337                    elseif id == disc_code or (id == kern_code and getsubtype(current) == fontkern_code) then
338                        last = current
339                    else
340                        if tag > 0 then
341                            head = inject(head,first,last,tag,line)
342                        end
343                        break
344                    end
345                    current = getnext(current)
346                    if current then
347                        id = getid(current)
348                    else
349                        if tag > 0 then
350                            head = inject(head,first,last,tag,line)
351                        end
352                        return head
353                    end
354                end
355            end
356            -- pick up (as id can have changed)
357            if id == hlist_code or id == vlist_code then
358                local list = getlist(current)
359                if list then
360                    local l = collect(list)
361                    if l ~= list then
362                        setlist(current,l)
363                    end
364                end
365            end
366            current = getnext(current)
367        end
368        return head
369    end
370
371end
372
373local collect_max  do
374
375    local function inject(parent,head,first,last,tag,line)
376        local w, h, d = getrangedimensions(parent,first,getnext(last))
377        if h < height then
378            h = height
379        end
380        if d < depth then
381            d = depth
382        end
383        if trace then
384            head = insertbefore(head,first,new_rule(w,fulltrace and h or traceheight,fulltrace and d or tracedepth))
385            head = insertbefore(head,first,new_kern(-w))
386        end
387        head = x_hlist(head,first,tag,line,w,h,d)
388        return head
389    end
390
391    collect_max = function(head,parent)
392        local current = head
393        while current do
394            local id = getid(current)
395            if id == glyph_code then
396                local first = current
397                local last  = current
398                local tag   = 0
399                local line  = 0
400                while true do
401                    if id == glyph_code then
402                        local tc, lc = getinputfields(current)
403                        if tc and tc > 0 then
404                            if tag > 0 and (tag ~= tc or line ~= lc) then
405                                head  = inject(parent,head,first,last,tag,line)
406                                first = current
407                            end
408                            tag  = tc
409                            line = lc
410                            last = current
411                        else
412                            if tag > 0 then
413                                head = inject(parent,head,first,last,tag,line)
414                                tag  = 0
415                            end
416                            first = nil
417                            last  = nil
418                        end
419                    elseif id == disc_code then
420                        if not first then
421                            first = current
422                        end
423                        last = current
424                    elseif id == kern_code and getsubtype(current) == fontkern_code then
425                        if first then
426                            last = current
427                        end
428                    elseif id == glue_code then
429                     -- if tag > 0 then
430                     --     local tc, lc = getinputfields(current)
431                     --     if tc and tc > 0 then
432                     --         if tag ~= tc or line ~= lc then
433                     --             head = inject(parent,head,first,last,tag,line)
434                     --             tag  = 0
435                     --             break
436                     --         end
437                     --     else
438                     --         head = inject(parent,head,first,last,tag,line)
439                     --         tag  = 0
440                     --         break
441                     --     end
442                     -- else
443                     --     tag = 0
444                     --     break
445                     -- end
446                     -- id = nil -- so no test later on
447                    elseif id == penalty_code then
448                        -- go on (and be nice for math)
449                    else
450                        if tag > 0 then
451                            head = inject(parent,head,first,last,tag,line)
452                            tag  = 0
453                        end
454                        break
455                    end
456                    current = getnext(current)
457                    if current then
458                        id = getid(current)
459                    else
460                        if tag > 0 then
461                            head = inject(parent,head,first,last,tag,line)
462                        end
463                        return head
464                    end
465                end
466            end
467            -- pick up (as id can have changed)
468            if id == hlist_code or id == vlist_code then
469                local list = getlist(current)
470                if list then
471                    local l = collect(list,current)
472                    if l and l ~= list then
473                        setlist(current,l)
474                    end
475                end
476            end
477            current = getnext(current)
478        end
479        return head
480    end
481
482end
483
484collect = collect_max
485
486function synctex.collect(head,where)
487    if enabled and where ~= "object" then
488        return collect(head,head)
489    else
490        return head
491    end
492end
493
494-- also no solution for bad first file resolving in sumatra
495
496function synctex.start()
497    if enabled then
498        nofsheets = nofsheets + 1 -- could be realpageno
499        if flushpreamble() then
500            writeanchor()
501            filehandle:write("{",nofsheets,eol)
502            -- this seems to work:
503         -- local pagewidth, pageheight = getpagedimensions()
504            filehandle:write(f_plist(1,0,0,0,0,0,0))
505        end
506    end
507end
508
509function synctex.stop()
510    if enabled then
511        filehandle:write(s_plist)
512        writeanchor()
513        filehandle:write("}",nofsheets,eol)
514        nofobjects = nofobjects + 2
515    end
516end
517
518local enablers  = { }
519local disablers = { }
520
521function synctex.registerenabler(f)
522    enablers[#enablers+1] = f
523end
524
525function synctex.registerdisabler(f)
526    disablers[#disablers+1] = f
527end
528
529function synctex.enable(use_rule)
530    if not never and not enabled then
531        enabled = true
532        userule = use_rule
533        setinputstatemode(3) -- we want details
534        if not used then
535            nodes.tasks.enableaction("shipouts","luatex.synctex.collect")
536            report_system("synctex functionality is enabled, expect 5-10 pct runtime overhead!")
537            used = true
538        end
539        for i=1,#enablers do
540            enablers[i](true)
541        end
542    end
543end
544
545function synctex.disable()
546    if enabled then
547        setinputstatemode(0)
548        report_system("synctex functionality is disabled!")
549        enabled = false
550        for i=1,#disablers do
551            disablers[i](false)
552        end
553    end
554end
555
556function synctex.finish()
557    if enabled then
558        flushpostamble()
559    else
560        makenames()
561        removefile(sncfile)
562    end
563end
564
565local filename = nil
566
567function synctex.pause()
568    paused = paused + 1
569    if enabled and paused == 1 then
570        setinputstatemode(0)
571    end
572end
573
574function synctex.resume()
575    if enabled and paused == 1 then
576        setinputstatemode(3)
577    end
578    paused = paused - 1
579end
580
581-- not the best place
582
583luatex.registerstopactions(synctex.finish)
584
585statistics.register("synctex tracing",function()
586    if used then
587        return string.format("%i referenced files, %i files ignored, %i objects flushed, logfile: %s",
588            noftags,nofblocked,nofobjects,sncfile)
589    end
590end)
591
592local implement = interfaces.implement
593local variables = interfaces.variables
594
595function synctex.setup(t)
596    if t.state == variables.never then
597        synctex.disable() -- just in case
598        never = true
599        return
600    end
601    if t.method == variables.max then
602        collect = collect_max
603    else
604        collect = collect_min
605    end
606    if t.state == variables.start then
607        synctex.enable(false)
608    elseif t.state == variables["repeat"] then
609        synctex.enable(true)
610    else
611        synctex.disable()
612    end
613end
614
615implement {
616    name      = "synctexblockfilename",
617    arguments = "string",
618    actions   = synctex.blockfilename,
619}
620
621implement {
622    name      = "synctexsetfilename",
623    arguments = "string",
624    actions   = synctex.setfilename,
625}
626
627implement {
628    name      = "synctexresetfilename",
629    actions   = synctex.resetfilename,
630}
631
632implement {
633    name      = "setupsynctex",
634    actions   = synctex.setup,
635    arguments = {
636        {
637            { "state" },
638            { "method" },
639        },
640    },
641}
642
643implement {
644    name    = "synctexpause",
645    actions = synctex.pause,
646}
647
648implement {
649    name    = "synctexresume",
650    actions = synctex.resume,
651}
652
653implement {
654    name    = "synctexpushline",
655    actions = synctex.pushline,
656}
657
658implement {
659    name    = "synctexpopline",
660    actions = synctex.popline,
661}
662
663implement {
664    name    = "synctexdisable",
665    actions = synctex.disable,
666}
667