node-ppt.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['node-ppt'] = {
    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"
}

-- This is all very exeperimental and likely to change.

local next, type, unpack, load = next, type, table.unpack, load

local serialize = table.serialize
local formatters = string.formatters

local report           = logs.reporter("properties")
local report_setting   = logs.reporter("properties","setting")
local trace_setting    = false trackers.register("properties.setting", function(v) trace_setting = v end)

-- report("using experimental properties")

local nuts             = nodes.nuts
local tonut            = nuts.tonut
local tonode           = nuts.tonode
local getid            = nuts.getid
local getnext          = nuts.getnext
local getprev          = nuts.getprev
local getsubtype       = nuts.getsubtype
local getlist          = nuts.getlist
local setlist          = nuts.setlist
local getprop          = nuts.getprop

local removenode       = nuts.remove

local nextnode         = nuts.traversers.node
local nextwhatsit      = nuts.traversers.whatsit

local nodecodes        = nodes.nodecodes
local whatsitcodes     = nodes.whatsitcodes

local whatsit_code     = nodecodes.whatsit
local hlist_code       = nodecodes.hlist
local vlist_code       = nodecodes.vlist

local userdefinedwhatsit_code = whatsitcodes.userdefined

local nodepool         = nodes.pool
local new_usernode     = nodepool.usernode

local variables        = interfaces.variables
local v_before         = variables.before
local v_after          = variables.after
local v_here           = variables.here

local property_id      = nodepool.userids["property"]

local properties       = nodes.properties
local propertydata     = properties.data

local starttiming      = statistics.starttiming
local stoptiming       = statistics.stoptiming

if not propertydata then
    return
end

-- management

local function register(where,data,...)
    if not data then
        data  = where
        where = v_after
    end
    if data then
        nofslots = nofslots + 1
        return new_usernode(property_id,{ where, data, ... })
    end
end

local writenode = node.write
local flushnode = context.nodes.flush

function commands.deferredproperty(...)
--  context(register(...))
    flushnode(register(...))
end

function commands.immediateproperty(...)
    writenode(register(...))
end

commands.attachproperty = commands.deferredproperty

local actions = { } properties.actions = actions

table.setmetatableindex(actions,function(t,k)
    report("unknown property action %a",k)
    return function() end
end)

local f_delayed   = formatters["return function(target,head,where,propdata,parent) %s end"]
local f_immediate = formatters["return function(target,head,where,propdata) %s end"]

local nofdelayed  = 0 -- better is to keep track of it per page ... we can have deleted nodes with properties
local nofslots    = 0

function actions.delayed(target,head,where,propdata,code,...) -- this one is used at the tex end
    if code then
        local delayed = propdata.delayed
        if delayed then
            delayed[#delayed+1] = { where, code, ... }
        else
            propdata.delayed = { { where, code, ... } }
            nofdelayed = nofdelayed + 1
        end
    end
end

function actions.fdelayed(target,head,where,propdata,code,...) -- this one is used at the tex end
    if code then
        local delayed = propdata.delayed
        if delayed then
            delayed[#delayed+1] = { false, code, ... }
        else
            propdata.delayed = { { false, code, ... } }
            nofdelayed = nofdelayed + 1
        end
    end
end

function actions.immediate(target,head,where,propdata,code,...) -- this one is used at the tex end
    local kind = type(code)
    if kind == "string" then
        local f = f_immediate(code)
        local okay, err = load(f)
        if okay then
            local h = okay()(target,head,where,propdata,...)
            if h and h ~= head then
                return h
            end
        end
    elseif kind == "function" then
        local h = code()(target,head,where,propdata,...)
        if h and h ~= head then
            return h
        end
    end
end

local function delayed(head,parent) -- direct based
    for target, id in nextnode, head do
        local p = propertydata[target]
        if p then
            local delayed = p.delayed
            if delayed then
                for i=1,#delayed do
                    local d = delayed[i]
                    local code  = d[2]
                    local kind = type(code)
                    if kind == "string" then
                        code, err = load(f_delayed(code))
                        if code then
                            code = code()
                        end
                    end
                    local where = d[1]
                    if where then
                        local h = code(target,where,head,p,parent,unpack(d,3)) -- target where propdata head parent
                        if h and h ~= head then
                            head = h
                        end
                    else
                        code(unpack(d,3))
                    end
                end
                p.delayed = nil
                if nofdelayed == 1 then
                    nofdelayed = 0
                    return head
                else
                    nofdelayed = nofdelayed - 1
                end
            end
        end
        if id == hlist_code or id == vlist_code then
            local list = getlist(target)
            if list then
                local done = delayed(list,parent)
                if done then
                    setlist(target,done)
                end
                if nofdelayed == 0 then
                    return head
                end
            end
        else
            -- maybe also some more lists? but we will only use this for some
            -- special cases .. who knows
        end
    end
    return head
end

function properties.delayed(head) --
    if nofdelayed > 0 then
     -- if next(propertydata) then
            starttiming(properties)
            head = delayed(head)
            stoptiming(properties)
     -- else
     --     delayed = 0
     -- end
    end
    return head
end

-- more explicit ones too

local anchored = {
    [v_before] = function(n)
        while n do
            n = getprev(n)
            if getid(n) == whatsit_code and getsubtype(n) == user_code and getprop(n,"id") == property_id then
                -- continue
            else
                return n
            end
        end
    end,
    [v_after] = function(n)
        while n do
            n = getnext(n)
            if getid(n) == whatsit_code then
                local subtype = getsubtype(n)
                if (subtype == userdefinedwhatsit_code and getprop(n,"id") == property_id) then
                    -- continue
                else
                    return n
                end
            else
                return n
            end
        end
    end,
    [v_here] = function(n)
        -- todo
    end,
}

table.setmetatableindex(anchored,function(t,k)
    local v = anchored[v_after]
    t[k] = v
    return v
end)

function properties.attach(head)

    if nofslots <= 0 then
        return head
    end

    local last = nil

    starttiming(properties)

    for source, subtype in nextwhatsit, head do
        if subtype == userdefinedwhatsit_code then
            if last then
                removenode(head,last,true)
                last = nil
            end
            if getprop(source,"id") == property_id then
                local data = getprop(source,"data")
                if data then
                    local where  = data[1]
                    local target = anchored[where](source)
                    if target then
                        local first    = data[2]
                        local method   = type(first)
                        local p_target = propertydata[target]
                        local p_source = propertydata[source]
                        if p_target then
                            if p_source then
                                for k, v in next, p_source do
                                    p_target[k] = v
                                end
                            end
                            if method == "table" then
                                for k, v in next, first do
                                    p_target[k] = v
                                end
                            elseif method == "function" then
                                first(target,head,where,p_target,unpack(data,3))
                            elseif method == "string" then
                                actions[first](target,head,where,p_target,unpack(data,3))
                            end
                        elseif p_source then
                            if method == "table" then
                                propertydata[target] = p_source
                                for k, v in next, first do
                                    p_source[k] = v
                                end
                            elseif method == "function" then
                                propertydata[target] = p_source
                                first(target,head,where,p_source,unpack(data,3))
                            elseif method == "string" then
                                propertydata[target] = p_source
                                actions[first](target,head,where,p_source,unpack(data,3))
                            end
                        else
                            if method == "table" then
                                propertydata[target] = first
                            elseif method == "function" then
                                local t = { }
                                propertydata[target] = t
                                first(target,head,where,t,unpack(data,3))
                            elseif method == "string" then
                                local t = { }
                                propertydata[target] = t
                                actions[first](target,head,where,t,unpack(data,3))
                            end
                        end
                        if trace_setting then
                            report_setting("node %i, id %s, data %s",
                                target,nodecodes[getid(target)],serialize(propertydata[target],false))
                        end
                    end
                    if nofslots == 1 then
                        nofslots = 0
                        last = source
                        break
                    else
                        nofslots = nofslots - 1
                    end
                end
                last = source
            end
        end
    end

    if last then
        removenode(head,last,true)
    end

    stoptiming(properties)

    return head

end

-- maybe better hard coded in-place

statistics.register("properties processing time", function()
    return statistics.elapsedseconds(properties)
end)

-- only for development

-- local tasks = nodes.tasks
--
-- local function show(head,level,report)
--     for target in nextnode, head do
--         local p = propertydata[target]
--         if p then
--             report("level %i, node %i, id %s, data %s",
--                 level,target,nodecodes[getid(target)],serialize(propertydata[target],false))
--         end
--         local id = getid(target)
--         if id == hlist_code or id == vlist_code then
--             local list = getlist(target)
--             if list then
--                 show(list,level+1,report)
--             end
--         else
--             -- maybe more lists
--         end
--     end
--     return head, false
-- end
--
-- local report_shipout    = logs.reporter("properties","shipout")
-- local report_processors = logs.reporter("properties","processors")
--
-- function properties.showshipout   (head) return tonode(show(tonut(head),1,report_shipout   )), true end
-- function properties.showprocessors(head) return tonode(show(tonut(head),1,report_processors)), true end
--
-- tasks.prependaction("shipouts","before","nodes.properties.showshipout")
-- tasks.disableaction("shipouts","nodes.properties.showshipout")
--
-- trackers.register("properties.shipout",function(v)
--     tasks.setaction("shipouts","nodes.properties.showshipout",v)
-- end)
--
-- tasks.appendaction ("processors","after","nodes.properties.showprocessors")
-- tasks.disableaction("processors","nodes.properties.showprocessors")
--
-- trackers.register("properties.processors",function(v)
--     tasks.setaction("processors","nodes.properties.showprocessors",v)
-- end)