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 gluecodes           = nodes.gluecodes

local glyph_code          <const> = nodecodes.glyph
local disc_code           <const> = nodecodes.disc
local glue_code           <const> = nodecodes.glue
local penalty_code        <const> = nodecodes.penalty
local boundary_code       <const> = nodecodes.boundary
local kern_code           <const> = nodecodes.kern
local hlist_code          <const> = nodecodes.hlist
local vlist_code          <const> = nodecodes.vlist
local fontkern_code       <const> = kerncodes.fontkern
local spaceskip_code      <const> = gluecodes.spaceskip
local xspaceskip_code     <const> = gluecodes.xspaceskip

local insertbefore        = nuts.insertbefore
local insertafter         = nuts.insertafter

local nextnode            = nuts.traversers.node

local nodepool            = nuts.pool
local new_latelua         = nodepool.latelua
local new_kern            = nodepool.kern
local new_rule            = nodepool.rule
local new_outline_rule    = nodepool.outlinerule

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        <const>  = "\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    <const>  = ")\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    <const>  = "]\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 usedsncfile = 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()
    usedsncfile = replacesuffix(tex.jobname,"synctex")
end

local function flushpreamble()
    makenames()
    filehandle = openfile(usedsncfile,"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()
    usedsncfile = 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 == glue_code and getsubtype(current) == spaceskip_code then -- xspaceskip
                    elseif id == glue_code then
                    elseif id == penalty_code then
                    elseif id == boundary_code then
                    elseif id == kern_code and getsubtype(current) == fontkern_code then -- maybe italic too
                        if first then
                            last = current
                        end
                    else -- which whatsits can we skip
                        -- rule whatsit
--                     print(nodecodes[id])
                        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

-- In mkxl we have control over the shipout so we can comfortably start and stop
-- outside the node list.

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

function synctex.collect(head,where)
    if enabled and where ~= "object" then
        return collect(head,head)
    else
        return head
    end
end

-- do
--
--    local function start()
--        if enabled 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
--
--    local function stop()
--        if enabled then
--            filehandle:write(s_plist)
--            writeanchor()
--            filehandle:write("}",nofsheets,eol)
--            nofobjects = nofobjects + 2
--        end
--    end
--
--    synctex.start = function() end
--    synctex.stop  = function() end
--
--    local findtail = nuts.tail
--
--    function synctex.collect(head,where)
--        if enabled and where ~= "object" then
--            nofsheets = nofsheets + 1 -- could be realpageno
--            if flushpreamble() then
--                local list = getlist(head)
--                if list then
--                    list = insertbefore(list,list,new_latelua(start))
--                    insertafter(list,findtail(list),new_latelua(stop))
--                    setlist(head,list)
--                    head = collect(head,head)
--                end
--            end
--        end
--        return head
--    end
--
--end

local enablers  = { }
local disablers = { }

function synctex.registerenabler(f)
    enablers[#enablers+1] = f
end

function synctex.registerdisabler(f)
    disablers[#disablers+1] = f
end

statistics.feedback.register { name = "synctex", index = 3, category = "performance" }

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()
        statistics.feedback.setstate("synctex",true)
    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(usedsncfile)
    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

function synctex.isenabled()
    return enabled
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,usedsncfile)
    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,
}
