luat-cbk.lmt /size: 8832 b    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['luat-cbk'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.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
9local insert, remove, concat = table.insert, table.remove, table.concat
10local find, format = string.find, string.format
11local collectgarbage, type, next = collectgarbage, type, next
12local round = math.round
13local sortedhash, sortedkeys, tohash = table.sortedhash, table.sortedkeys, table.tohash
14
15-- Callbacks are the real asset of LuaTeX. They permit you to hook your own code
16-- into the TeX engine. Here we implement a few handy auxiliary functions. Watch
17-- out, there are diferences between LuateX and LuaMetaTeX.
18
19callbacks       = callbacks or { }
20local callbacks = callbacks
21
22-- When you (temporarily) want to install a callback function, and after a while
23-- wants to revert to the original one, you can use the following two functions.
24-- This only works for non-frozen ones.
25
26local trace_callbacks   = false  trackers.register("system.callbacks", function(v) trace_callbacks = v end)
27local trace_calls       = false  -- only used when analyzing performance and initializations
28local trace_checking    = false  trackers.register("memory.checking", function(v) trace_checking = v end)
29
30local report_system     = logs.reporter("system")
31local report_callbacks  = logs.reporter("system","callbacks")
32local report_memory     = logs.reporter("system","memory")
33
34local register_callback = callback.register
35local find_callback     = callback.find
36local list_callbacks    = callback.list
37local register_usercall = false
38local original_register = register_callback
39
40local frozen            = { }
41local stack             = { }
42local list              = callbacks.list
43local permit_overloads  = false
44local block_overloads   = false
45
46-- By now most callbacks are frozen and most provide a way to plug in your own code.
47-- For instance all node list handlers provide before/after namespaces and the file
48-- handling code can be extended by adding schemes and if needed I can add more
49-- hooks. So there is no real need to overload a core callback function. It might be
50-- ok for quick and dirty testing but anyway you're on your own if you permanently
51-- overload callback functions.
52
53-- This might become a configuration file only option when it gets abused too much.
54
55directives.register("system.callbacks.permitoverloads", function(v)
56    if block_overloads or permit_overloads then
57        -- once bad news, always bad news
58    elseif v then
59        permit_overloads = { }
60        report_system()
61        report_system("The callback system has been brought in an unprotected state. As a result of directly")
62        report_system("setting of callbacks subsystems of ConTeXt can stop working. There is no support for")
63        report_system("bugs resulting from this state. It's better to use the official extension mechanisms.")
64        report_system()
65    end
66end)
67
68sandbox.initializer {
69    category = "functions",
70    action   = function()
71        block_overloads = true
72    end
73}
74
75if not list then -- otherwise counters get reset
76
77    list = utilities.storage.allocate(list_callbacks())
78
79    local supported = { }
80
81    for k in next, list do
82        list[k]      = 0
83        supported[k] = true
84    end
85
86    callbacks.list      = list
87    callbacks.supported = supported
88
89end
90
91local delayed = tohash {
92    "buildpage_filter",
93}
94
95if trace_calls then
96
97    local functions = { }
98
99    register_callback = function(name,func)
100        if type(func) == "function" then
101            if functions[name] then
102                functions[name] = func
103                return find_callback(name)
104            else
105                functions[name] = func
106                local cnuf = function(...)
107                    list[name] = list[name] + 1
108                    return functions[name](...)
109                end
110                return original_register(name,cnuf)
111            end
112        else
113            return original_register(name,func)
114        end
115    end
116
117end
118
119-- temporary, not public:
120
121callbacks.functions = { }
122
123-- till here
124
125local reported = { }
126
127local function register_usercall(what,name,func)
128    if list[name] then
129        if trace_callbacks or not reported[name] then
130            report_system()
131            report_system("disabling core code by %s user function into callback '%s' (reported only once)",what,name)
132            report_system()
133            reported[name] = true
134        end
135        permit_overloads[name] = true
136        return original_register(name,function(...)
137            if trace_callbacks then
138                report_callbacks("calling user function from '%s'",name)
139            end
140            return func(...)
141        end)
142    else
143        report_callbacks("not %s function into invalid callback '%s'",name)
144        return nil, format("unknown callback '%s'",name)
145    end
146end
147
148local function frozen_callback(name)
149    report_callbacks("not %s frozen %a","registering",name)
150    return nil, format("callback '%s' is frozen",name) -- no formatter yet
151end
152
153local function state(name)
154    local f = find_callback(name)
155    if f == false then
156        return "disabled"
157    elseif f then
158        return "enabled"
159    else
160        return "undefined"
161    end
162end
163
164function callbacks.known(name)
165    return list[name]
166end
167
168function callbacks.report()
169    local usage = list_callbacks()
170    for name, _ in sortedhash(list) do
171        local str = frozen[name]
172        local sta = state(name)
173        local use = usage[name] and "+" or "-"
174        if str then
175            report_callbacks("%-9s : %s : %-22s -> %s",sta,use,name,str)
176        else
177            report_callbacks("%-9s : %s : %-22s",      sta,use,name)
178        end
179    end
180end
181
182function callbacks.freeze(name,freeze)
183    if not permit_overloads then
184        freeze = type(freeze) == "string" and freeze
185        if find(name,"*",1,true) then
186            local pattern = name
187            for name, _ in next, list do
188                if find(name,pattern) then
189                    frozen[name] = freeze or frozen[name] or "frozen"
190                end
191            end
192        else
193            frozen[name] = freeze or frozen[name] or "frozen"
194        end
195    end
196end
197
198function callbacks.register(name,func,freeze)
199    if frozen[name] then
200        if permit_overloads then
201            return register_usercall("registering",name,func)
202        else
203            return frozen_callback(name)
204        end
205    elseif freeze then
206        frozen[name] = type(freeze) == "string" and freeze or "registered"
207    end
208    if delayed[name] and environment.initex then
209        return nil
210    end
211    return register_callback(name,func)
212end
213
214function callback.register(name,func) -- original
215    if not frozen[name] then
216        return register_callback(name,func)
217    elseif permit_overloads then
218        return register_usercall("registering",name,func)
219    else
220        return frozen_callback(name)
221    end
222end
223
224function callbacks.push(name,func)
225    if not frozen[name] or permit_overloads then
226        local sn = stack[name]
227        if not sn then
228            sn = { }
229            stack[name] = sn
230        end
231        insert(sn,find_callback(name))
232        if permit_overloads then
233            register_usercall("pushing",name,func)
234        else
235            register_callback(name,func)
236        end
237    else
238        report_callbacks("not %s frozen %a","pushing",name)
239    end
240end
241
242function callbacks.pop(name)
243    if not frozen[name] or permit_overloads then
244        local sn = stack[name]
245        if not sn or #sn == 0 then
246            -- some error
247            register_callback(name,nil) -- ! really needed
248        else
249         -- this fails: register_callback(name, remove(stack[name]))
250            local func = remove(sn)
251            register_callback(name,func)
252        end
253    end
254end
255
256if trace_calls then
257    statistics.register("callback details", function()
258        local t = { } -- todo: pass function to register and quit at nil
259        for name, n in sortedhash(list) do
260            if n > 0 then
261                t[#t+1] = format("%s -> %s",name,n)
262            end
263        end
264        return concat(t," ")
265    end)
266end
267
268statistics.register("callbacks overloaded by user", function()
269    if permit_overloads then
270        return concat(sortedkeys(permit_overloads)," ")
271    end
272end)
273
274-- this will move to a module
275
276commands = commands or { }
277
278function commands.showcallbacks()
279    local NC, NR, verbatim = context.NC, context.NR, context.type
280    context.starttabulate { "|l|l|p|" }
281    for name, _ in sortedhash(list) do
282        NC() verbatim(name) NC() verbatim(state(name)) NC() context(frozen[name] or "") NC() NR()
283    end
284    context.stoptabulate()
285end
286