mtx-synctex.lua /size: 14 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['mtx-synctex'] = {
2    version   = 1.002,
3    comment   = "companion to mtxrun.lua",
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-- InverseSearchCmdLine = scite.exe "%f" "-goto:%l" $
10-- InverseSearchCmdLine = mtxrun.exe --script synctex --edit --name="%f" --line="%l" $
11
12local tonumber = tonumber
13local find, match, gsub, formatters = string.find, string.match, string.gsub, string.formatters
14local isfile = lfs.isfile
15local max = math.max
16local longtostring = string.longtostring
17
18local helpinfo = [[
19<?xml version="1.0"?>
20<application>
21 <metadata>
22  <entry name="name">mtx-synctex</entry>
23  <entry name="detail">SyncTeX Checker</entry>
24  <entry name="version">1.01</entry>
25 </metadata>
26 <flags>
27  <category name="basic">
28   <subcategory>
29    <flag name="edit"><short>open file at line: --line=.. --editor=.. sourcefile</short></flag>
30    <flag name="list"><short>show all areas: synctexfile</short></flag>
31    <flag name="goto"><short>open file at position: --page=.. --x=.. --y=.. [--tolerance=] --editor=.. synctexfile</short></flag>
32    <flag name="report"><short>show (tex) file and line: [--direct] --page=.. --x=.. --y=.. [--tolerance=] --console synctexfile</short></flag>
33    <flag name="find"><short>find (pdf) page and box: [--direct] --file=.. --line=.. synctexfile</short></flag>
34   </subcategory>
35  </category>
36 </flags>
37</application>
38]]
39
40local application = logs.application {
41    name     = "mtx-synctex",
42    banner   = "ConTeXt SyncTeX Checker 1.01",
43    helpinfo = helpinfo,
44}
45
46local report = application.report
47
48local template_show = "page=%i llx=%r lly=%r urx=%r ury=%r"
49local template_goto = "filename=%a linenumber=%a tolerance=%a"
50
51local function reportdirect(template,...)
52    print(formatters[template](...))
53end
54
55local editors = {
56    console = function(specification)
57        print(string.formatters["%q %i %i"](specification.filename,specification.linenumber or 1,specification.tolerance))
58    end,
59    scite = sandbox.registerrunner {
60        name     = "scite",
61        program  = {
62            windows = "scite",
63            unix    = "SciTE",
64        },
65        template = longtostring [[
66            "%filename%"
67            "-goto:%linenumber%"
68        ]],
69    },
70}
71
72local function validfile(filename)
73    if not filename or not isfile(filename) then
74        report("invalid synctex log file %a",filename)
75        return false
76    else
77        return true
78    end
79end
80
81local function editfile(filename,line,tolerance,editor)
82    if not validfile(filename) then
83        return
84    end
85    local runner = editors[editor or "scite"] or editors.scite
86    runner {
87        filename   = filename,
88        linenumber = tonumber(line) or 1,
89        tolerance  = tolerance,
90    }
91end
92
93-- In context we only care about explicitly marked horizontal blobs. And this is
94-- only a check script. I know of no viewer that calls the synctex command line
95-- version. Otherwise we could provide our own simplified variant and even
96-- consider a more compact format (for instance we could use an "=" when the value
97-- of x y w h d is the same as before, which is actually often the case for y, h
98-- and d).
99
100local factor = (7200/7227)/65536 -- we assume unit 1
101local quit   = true              -- we only have one hit anyway
102
103local function findlocation(filename,page,xpos,ypos)
104    if not validfile(filename) then
105        return
106    elseif not page then
107        page = 1
108    elseif not xpos or not ypos then
109        report("provide x and y coordinates (unit: basepoints)")
110        return
111    end
112    local files = { }
113    local found = false
114    local skip  = false
115    local dx    = false
116    local dy    = false
117    local px    = xpos / factor
118    local py    = ypos / factor
119    local fi    = 0
120    local ln    = 0
121    for line in io.lines(filename) do
122        if found then
123            if find(line,"^}") then
124                break
125            else
126                -- we only look at positive cases
127                local f, l, x, y, w, h, d = match(line,"^h(.-),(.-):(.-),(.-):(.-),(.-),(.-)$")
128                if f and f ~= 0 then
129                    x = tonumber(x)
130                    if px >= x then
131                        w = tonumber(w)
132                        if px <= x + w then
133                            y = tonumber(y)
134                            d = tonumber(d)
135                            if py >= y - d then
136                                h = tonumber(h)
137                                if py <= y + h then
138                                    if quit then
139                                        -- we have no overlapping boxes
140                                        fi = f
141                                        ln = l
142                                        break
143                                    else
144                                        local lx = px - x
145                                        local rx = x + w - px
146                                        local by = py - y + d
147                                        local ty = y + h - py
148                                        mx = lx < rx and lx or rx
149                                        my = by < ty and by or ty
150                                        if not dx then
151                                            dx = mx
152                                            dy = my
153                                            fi = f
154                                            ln = l
155                                        else
156                                            if mx < dx then
157                                                dx = mx
158                                                di = f
159                                                ln = l
160                                            end
161                                            if my < dy then
162                                                dy = my
163                                                fi = f
164                                                ln = l
165                                            end
166                                        end
167                                    end
168                                end
169                            end
170                        end
171                    end
172                end
173            end
174        elseif skip then
175            if find(line,"^}") then
176                skip = false
177            end
178        elseif find(line,"^{(%d+)") then
179            local p = tonumber(match(line,"^{(%d+)"))
180            if p == page then
181                found = true
182            else
183                skip = true
184            end
185        elseif find(line,"^Input:") then
186            local id, name = match(line,"^Input:(.-):(.-)$")
187            if id then
188                files[id] = name
189            end
190        end
191    end
192    if fi ~= 0 then
193        return files[fi], ln
194    end
195end
196
197local function findlocation(filename,page,xpos,ypos,tolerance)
198    if not validfile(filename) then
199        return
200    elseif not page then
201        page = 1
202    elseif not xpos or not ypos then
203        report("provide x and y coordinates (unit: basepoints)")
204        return
205    end
206    local files = { }
207    local found = false
208    local skip  = false
209    local fi    = 0
210    local ln    = 0
211    local tl    = 0
212    local lines = { }
213    for line in io.lines(filename) do
214        if found then
215            if find(line,"^}") then
216                local function locate(x,y)
217                    local dx = false
218                    local dy = false
219                    local px = (xpos + x) / factor
220                    local py = (ypos + y) / factor
221                    for i=1,#lines do
222                        local line = lines[i]
223                        -- we only look at positive cases
224                        local f, l, x, y, w, h, d = match(line,"^h(.-),(.-):(.-),(.-):(.-),(.-),(.-)$")
225                        if f and f ~= 0 then
226-- print(x,y,f)
227                            x = tonumber(x)
228                            if px >= x then
229                                w = tonumber(w)
230                                if px <= x + w then
231                                    y = tonumber(y)
232                                    d = tonumber(d)
233                                    if py >= y - d then
234                                        h = tonumber(h)
235                                        if py <= y + h then
236                                            if quit then
237                                                -- we have no overlapping boxes
238                                                fi = f
239                                                ln = l
240                                                return
241                                            else
242                                                local lx = px - x
243                                                local rx = x + w - px
244                                                local by = py - y + d
245                                                local ty = y + h - py
246                                                mx = lx < rx and lx or rx
247                                                my = by < ty and by or ty
248                                                if not dx then
249                                                    dx = mx
250                                                    dy = my
251                                                    fi = f
252                                                    ln = l
253                                                else
254                                                    if mx < dx then
255                                                        dx = mx
256                                                        di = f
257                                                        ln = l
258                                                    end
259                                                    if my < dy then
260                                                        dy = my
261                                                        fi = f
262                                                        ln = l
263                                                    end
264                                                end
265                                            end
266                                        end
267                                    end
268                                end
269                            end
270                        end
271                    end
272                end
273                locate(0,0)
274                if fi ~= 0 then
275                    return files[fi], ln, 0
276                end
277                if not tolerance then
278                    tolerance = 10
279                end
280                for s=1,tolerance,max(tolerance//10,1) do
281                    locate( s, 0) if fi ~= 0 then tl = s ; goto done end
282                    locate(-s, 0) if fi ~= 0 then tl = s ; goto done end
283                    locate( s, s) if fi ~= 0 then tl = s ; goto done end
284                    locate( s,-s) if fi ~= 0 then tl = s ; goto done end
285                    locate(-s, s) if fi ~= 0 then tl = s ; goto done end
286                    locate(-s,-s) if fi ~= 0 then tl = s ; goto done end
287                end
288                break
289            else
290                lines[#lines+1] = line
291            end
292        elseif skip then
293            if find(line,"^}") then
294                skip = false
295            end
296        elseif find(line,"^{(%d+)") then
297            local p = tonumber(match(line,"^{(%d+)"))
298            if p == page then
299                found = true
300            else
301                skip = true
302            end
303        elseif find(line,"^Input:") then
304            local id, name = match(line,"^Input:(.-):(.-)$")
305            if id then
306                files[id] = name
307            end
308        end
309    end
310 ::done::
311    if fi ~= 0 then
312        return files[fi], ln, tl
313    end
314end
315
316local function showlocation(filename,sourcename,linenumber,direct)
317    if not validfile(filename) then
318        return
319    end
320    local files = { }
321    local found = false
322    local page  = 0
323    if sourcename then
324        sourcename = file.collapsepath(sourcename)
325    end
326    for line in io.lines(filename) do
327        if found then
328            if find(line,"^}") then
329                found = false
330                if not sourcename then
331                    report("end page: %i",page)
332                end
333            else
334                local f, l, x, y, w, h, d = match(line,"^h(.-),(.-):(.-),(.-):(.-),(.-),(.-)$")
335                if f then
336                    x = tonumber(x)
337                    y = tonumber(y)
338                    local llx = factor * ( x               )
339                    local lly = factor * ( y - tonumber(d) )
340                    local urx = factor * ( x + tonumber(w) )
341                    local ury = factor * ( y + tonumber(h) )
342                    f = files[f]
343                    if not f then
344                        --
345                    elseif not sourcename then
346                        report("  [% 4r % 4r % 4r % 4r] : % 5i : %s",llx,lly,urx,ury,l,f)
347                    elseif f == sourcename and l == linenumber then
348                        (direct and reportdirect or report)(template_show,page,llx,lly,urx,ury)
349                        return
350                    end
351                end
352            end
353        elseif find(line,"^{(%d+)") then
354            page  = tonumber(match(line,"^{(%d+)"))
355            found = true
356            if not sourcename then
357                report("begin page: %i",page)
358            end
359        elseif find(line,"^Input:") then
360            local id, name = match(line,"^Input:(.-):(.-)$")
361            if id then
362                files[id] = name
363            end
364        end
365    end
366end
367
368local function gotolocation(filename,page,xpos,ypos,editor,direct,tolerance)
369    if filename then
370        local target, line, t = findlocation(filename,tonumber(page),tonumber(xpos),tonumber(ypos),tonumber(tolerance))
371        if target and line then
372            if editor then
373                editfile(target,line,t,editor)
374            else
375                (direct and reportdirect or report)(template_goto,target,line,t)
376            end
377        end
378    end
379end
380
381-- print(findlocation("oeps.synctex",4,318,348))
382-- print(findlocation("oeps.synctex",40,378,348))
383-- print(gotolocation("oeps.synctex",4,318,348,"scite"))
384-- print(showlocation("oeps.synctex"))
385
386local argument = environment.argument
387local filename = environment.files[1]
388
389if argument("edit") then
390    editfile(filename,argument("line"),argument("editor"))
391elseif argument("goto") then
392    gotolocation(filename,argument("page"),argument("x"),argument("y"),argument("editor"),argument("direct"),argument("tolerance"))
393elseif argument("report") then
394    gotolocation(filename,argument("page"),argument("x"),argument("y"),"console",argument("direct"),argument("tolerance"))
395elseif argument("list") then
396    showlocation(filename)
397elseif argument("find") then
398    showlocation(filename,argument("file"),argument("line"),argument("direct"))
399elseif argument("exporthelp") then
400    application.export(argument("exporthelp"),filename)
401else
402    application.help()
403end
404
405