cldf-ini.lua /size: 54 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['cldf-ini'] = {
2    version   = 1.001,
3    comment   = "companion to cldf-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-- todo: {token} -> 3 tokens
10
11-- This started as an experiment: generating context code at the lua end. After all
12-- it is surprisingly simple to implement due to metatables. I was wondering if
13-- there was a more natural way to deal with commands at the lua end. Of course it's
14-- a bit slower but often more readable when mixed with lua code. It can also be handy
15-- when generating documents from databases or when constructing large tables or so.
16--
17-- In cldf-tod.lua I have some code that might end up here. In cldf-old.lua the
18-- code that precedes more modern solutions (made possible by features in the engine),
19-- most noticeably function handling, which worked well for a decade, but by now the
20-- more efficient approach is stable enough to move the original code to the obsolete
21-- module.
22--
23-- maybe:  (escape) or 0x2061 (apply function) or 0x2394 (software function ⎔) (old)
24-- note : tex.print == line with endlinechar appended
25--
26-- During the crited project we ran into the situation that luajittex was 10-20 times
27-- slower that luatex ... after 3 days of testing and probing we finally figured out that
28-- the the differences between the lua and luajit hashers can lead to quite a slowdown
29-- in some cases.
30--
31-- context(lpeg.match(lpeg.patterns.texescape,"${}"))
32-- context(string.formatters["%!tex!"]("${}"))
33-- context("%!tex!","${}")
34--
35-- We try not to polute the context namespace too much. For that reason the commands are
36-- registered in the context.core namespace. You can call all context commands using
37-- context.foo etc. and pipe to context with context("foo"). Defining a local command
38-- foo being context.foo is okay, as is context.core.foo. We will have some definitions
39-- that are plugged into the core namespace (mostly for speedup) but otherwise specialized
40-- writers are in the context namespace only. In your documents you can best use the
41-- context.foo(...) and context(...) variant but in some core modules we use the faster
42-- ones in critical places (no one will notice of course). The standard syntax highlighter
43-- that I use knows how to visualize ctx related code.
44
45-- I cleaned this code up a bit when updating the cld manual but got distracted a bit by
46-- watching Trio Hiromi Uehara, Anthony Jackson & Simon Phillips (discovered via the
47-- later on YT). Hopefully the video breaks made the following better in the end.
48
49-- This module is also a test bed for experimental features so the content changes over
50-- time (for the better or worse). There have been no fundamental changes for many years
51-- and performance has not changed much either.
52
53-- The code here has evolved over time, and was already present in the early versions of
54-- mkiv. However, it got adapted when feature were added to luatex, like the ability to
55-- "print" nodes and later tokens. We use to have control over the method used (if only
56-- for testing) but the older code has been removed now. The history is in the archives.
57-- These were the controls:
58
59-- local tokenflushmode    = true
60-- local nodeflushmode     = true
61-- local scannerdefmode    = true
62-- local maxflushnodeindex = 0x10FFFF - 1
63
64-- In earlier experiments a function tables was referred to as lua.calls and the primitive
65-- \luafunctions was \luacall and we used our own implementation of a function table (more
66-- indirectness).
67
68local format, stripstring = string.format, string.strip
69local next, type, tostring, tonumber, unpack, select, rawset = next, type, tostring, tonumber, unpack, select, rawset
70local insert, remove, concat = table.insert, table.remove, table.concat
71local lpegmatch, lpegC, lpegS, lpegP, lpegV, lpegCc, lpegCs, patterns = lpeg.match, lpeg.C, lpeg.S, lpeg.P, lpeg.V, lpeg.Cc, lpeg.Cs, lpeg.patterns
72
73local formatters           = string.formatters -- using formatters is slower in this case
74local setmetatableindex    = table.setmetatableindex
75local setmetatablecall     = table.setmetatablecall
76local setmetatablenewindex = table.setmetatablenewindex
77
78context                 = context    or { }
79commands                = commands   or { }
80interfaces              = interfaces or { }
81
82local context           = context
83local commands          = commands
84local interfaces        = interfaces
85
86local loaddata          = io.loaddata
87
88local tex               = tex
89local texsprint         = tex.sprint    -- just appended (no space,eol treatment)
90local texprint          = tex.print     -- each arg a separate line (not last in directlua)
91----- texwrite          = tex.write     -- all 'space' and 'character'
92
93-- In this stage we don't yet have nodes populated so we access the library directly ...
94
95local isnode            = node.isnode   or node.is_node
96local copynodelist      = node.copylist or node.copy_list
97local writenode         = node.write
98local tonut             = node.direct.todirect
99local tonode            = node.direct.tonode
100
101local newtoken          = token.new
102----- createtoken       = token.create
103
104local istoken           = token.istoken or token.is_token
105local setluatoken       = token.setlua  or token.set_lua
106
107-- ... till here.
108
109local isprintable       = tex.isprintable or function(n)
110    return n and (type(n) == "string" or isnode(n) or istoken(n))
111end
112
113if tex.runlocal then
114    -- obselete names
115    tex.runtoks  = tex.runlocal
116    tex.quittoks = tex.quitlocal
117else
118    tex.runlocal  = tex.runtoks
119    tex.quitlocal = tex.quittoks
120end
121
122local catcodenumbers    = catcodes.numbers
123
124local ctxcatcodes       = catcodenumbers.ctxcatcodes
125local prtcatcodes       = catcodenumbers.prtcatcodes
126local texcatcodes       = catcodenumbers.texcatcodes
127local txtcatcodes       = catcodenumbers.txtcatcodes
128local vrbcatcodes       = catcodenumbers.vrbcatcodes
129local xmlcatcodes       = catcodenumbers.xmlcatcodes
130
131local flush             = texsprint   -- snippets
132local flushdirect       = texprint    -- lines
133----- flushraw          = texwrite
134
135local report_context    = logs.reporter("cld","tex")
136local report_cld        = logs.reporter("cld","stack")
137----- report_template   = logs.reporter("cld","template")
138
139local processlines      = true -- experiments.register("context.processlines", function(v) processlines = v end)
140
141local trialtypesetting  = function() return false end
142
143function context.settrialtypesettingmethod(f)
144    trialtypesetting         = f
145    context.trialtypesetting = f
146end
147
148context.trialtypesetting = function() return trialtypesetting() end -- can be aliased !
149
150local knownfunctions = (lua.getfunctionstable or lua.get_functions_table)(true)
151local showstackusage = false
152
153trackers.register("context.stack",function(v) showstackusage = v end)
154
155local freed    = { }
156local nofused  = 0
157local noffreed = 0
158
159local usedstack = function()
160    return nofused, noffreed
161end
162
163local flushfunction = function(slot,arg)
164    if arg() or trialtypesetting() then
165        -- keep
166    else
167        noffreed = noffreed + 1
168        freed[noffreed] = slot
169        knownfunctions[slot] = false
170    end
171end
172
173local storefunction = function(arg)
174    local f = function(slot) flushfunction(slot,arg) end
175    if noffreed > 0 then
176        local n = freed[noffreed]
177        freed[noffreed] = nil
178        noffreed = noffreed - 1
179        knownfunctions[n] = f
180        return n
181    else
182        nofused = nofused + 1
183        knownfunctions[nofused] = f
184        return nofused
185    end
186end
187
188local flushnode = function(slot,arg)
189    if trialtypesetting() then
190        writenode(copynodelist(arg))
191    else
192        writenode(arg)
193        noffreed = noffreed + 1
194        freed[noffreed] = slot
195        knownfunctions[slot] = false
196    end
197end
198
199local storenode = function(arg)
200    local f = function(slot) flushnode(slot,arg) end
201    if noffreed > 0 then
202        local n = freed[noffreed]
203        freed[noffreed] = nil
204        noffreed = noffreed - 1
205        knownfunctions[n] = f
206        return n
207    else
208        nofused = nofused + 1
209        knownfunctions[nofused] = f
210        return nofused
211    end
212end
213
214storage.storedfunctions = storage.storedfunctions or { }
215local storedfunctions   = storage.storedfunctions
216local initex            = environment.initex
217
218storage.register("storage/storedfunctions", storedfunctions, "storage.storedfunctions")
219
220local f_resolve = nil
221local p_resolve = ((1-lpegP("."))^1 / function(s) f_resolve = f_resolve[s] end * lpegP(".")^0)^1
222
223local function resolvestoredfunction(str)
224    if type(str) == "string" then
225        f_resolve = global -- namespace
226        lpegmatch(p_resolve,str)
227        return f_resolve
228    else
229        return str
230    end
231end
232
233local function expose(slot,f,...) -- so we can register yet undefined functions
234    local func = resolvestoredfunction(f)
235    if not func then
236        func = function() report_cld("beware: unknown function %i called: %s",slot,f) end
237    end
238    knownfunctions[slot] = func
239    return func(...)
240end
241
242if initex then
243    -- todo: log stored functions
244else
245    local slots = table.sortedkeys(storedfunctions)
246    local last  = #slots
247    if last > 0 then
248        -- we restore the references
249        for i=1,last do
250            local slot = slots[i]
251            local data = storedfunctions[slot]
252            knownfunctions[slot] = function(...)
253                -- print(data) -- could be trace
254                return expose(slot,data,...)
255            end
256        end
257        -- we now know how many are defined
258        nofused = slots[last]
259        -- normally there are no holes in the list yet
260        for i=1,nofused do
261            if not knownfunctions[i] then
262                noffreed = noffreed + 1
263                freed[noffreed] = i
264            end
265        end
266     -- report_cld("%s registered functions, %s freed slots",last,noffreed)
267    end
268end
269
270local registerfunction = function(f,direct,slot) -- either f=code or f=namespace,direct=name
271    local func
272    if slot then
273        -- already used
274    elseif noffreed > 0 then
275        slot = freed[noffreed]
276        freed[noffreed] = nil
277        noffreed = noffreed - 1
278    else
279        nofused = nofused + 1
280        slot = nofused
281    end
282    if direct then
283        if initex then
284            func = function(...) expose(slot,f,...) end
285            storedfunctions[slot] = f
286        else
287            func = resolvestoredfunction(f)
288        end
289        if type(func) ~= "function" then
290            func = function() report_cld("invalid resolve %A, case %s",f,1) end
291        end
292    elseif type(f) == "string" then
293        func = loadstring(f)
294        if type(func) ~= "function" then
295            func = function() report_cld("invalid code %A, case %s",f,2) end
296        end
297    elseif type(f) == "function" then
298        func = f
299    else
300        func = function() report_cld("invalid function %A, case %s",f,3) end
301    end
302    knownfunctions[slot] = func
303    return slot
304end
305
306local unregisterfunction = function(slot)
307    if knownfunctions[slot] then
308        noffreed = noffreed + 1
309        freed[noffreed] = slot
310        knownfunctions[slot] = false
311    else
312        report_cld("invalid function slot %A",slot)
313    end
314end
315
316local reservefunction = function()
317    if noffreed > 0 then
318        local n = freed[noffreed]
319        freed[noffreed] = nil
320        noffreed = noffreed - 1
321        return n
322    else
323        nofused = nofused + 1
324        return nofused
325    end
326end
327
328local callfunctiononce = function(slot)
329    knownfunctions[slot](slot)
330    noffreed = noffreed + 1
331    freed[noffreed] = slot
332    knownfunctions[slot] = false
333end
334
335setmetatablecall(knownfunctions,function(t,n) return knownfunctions[n](n) end)
336
337-- some protection
338
339do
340
341    local stub    = { }
342    local done    = false
343    local message = function()
344        -- one time message
345        if not done then
346            report_cld("")
347            report_cld("use : slot = context.functions.register(f)")
348            report_cld("and : context.functions.unregister(slot)")
349            report_cld("")
350            done = true
351        end
352    end
353
354    setmetatable(stub, {
355        __index    = message,
356        __newindex = message,
357    })
358
359    function lua.getfunctionstable()
360        message()
361        return stub
362    end
363
364    lua.get_functions_table = lua.getfunctionstable
365
366end
367
368-- The next hack is a convenient way to define scanners at the Lua end and get them
369-- available at the TeX end. There is some dirty magic needed to prevent overload
370-- during format loading. Nowadays we prefer to use the slightly less efficient way
371-- of defining interfaces using the implementer. There is a little more overhead in
372-- defining as well as runtime overhead, but we accept that.
373
374-- interfaces.scanners.foo = function() context("[%s]",tokens.scanners.string()) end : \scan_foo
375
376local storedscanners    = interfaces.storedscanners or { }
377local namesofscanners   = interfaces.namesofscanners or { }
378local interfacescanners = { }
379local privatenamespace  = "clf_"
380
381interfaces.storedscanners  = storedscanners
382interfaces.namesofscanners = namesofscanners
383
384storage.register("interfaces/storedscanners", storedscanners, "interfaces.storedscanners")
385
386-- local function registerscanner(name,action,protected,public,usage) -- todo: combine value and condition
387--     rawset(interfacescanners,name,action)
388--     local n = storedscanners[name]
389--     n = registerfunction("interfaces.scanners."..name,true,n)
390--     storedscanners[name] = n
391--     namesofscanners[n] = name
392--     name = public and name or (privatenamespace .. name)
393--  -- print(">>",name,protected and "protected" or "",usage or "macro")
394--     setluatoken(name,n,"global",protected and "protected" or "",usage or "macro")
395-- end
396
397-- todo: bitmap
398
399local registerscanner if CONTEXTLMTXMODE > 0 then
400
401    -- always permanent but we can consider to obey permanent==false
402
403    -- todo: make bitset instead of keys (nil is skipped anyway)
404
405    local function toflags(specification)
406        local protected = (specification.protected and "protected") or (specification.semiprotected and "semiprotected")
407        local untraced  = specification.untraced  and "untraced"
408        local usage     = specification.usage
409        if usage == "value" then
410            return "global", "value", "permanent", "untraced", protected
411        elseif usage == "condition" then
412            return "global", "conditional", "permanent", "untraced", protected
413        elseif specification.frozen then
414            return "global", "frozen", untraced, protected
415        elseif specification.permanent == false or specification.onlyonce then -- for now onlyonce here
416            return "global", untraced, protected, semiprotected
417        else
418            return "global", "permanent", untraced, protected
419        end
420    end
421
422    registerscanner = function(name,action,specification)
423        rawset(interfacescanners,name,action)
424        local n = registerfunction("interfaces.scanners."..name,true,storedscanners[name])
425        storedscanners[name] = n
426        namesofscanners[n] = name
427        name = specification.public and name or (privatenamespace .. name)
428     -- print(name,n,toflags(specification))
429        setluatoken(name,n,toflags(specification))
430    end
431
432else
433
434    registerscanner = function(name,action,specification)
435        rawset(interfacescanners,name,action)
436        local n = storedscanners[name]
437        n = registerfunction("interfaces.scanners."..name,true,n)
438        storedscanners[name] = n
439        namesofscanners[n] = name
440        name = specification.public and name or (privatenamespace .. name)
441        setluatoken(name,n,"global",specification.protected and "protected" or "")
442    end
443
444end
445
446interfaces.registerscanner = registerscanner
447
448function interfaces.knownscanner(name)
449    return interfacescanners[name]
450end
451
452function interfaces.nameofscanner(slot)
453    return namesofscanners[slot] or slot
454end
455
456if CONTEXTLMTXMODE > 0 then
457
458    callbacks.register("show_lua_call", function(what, slot)
459        local name = namesofscanners[slot]
460     -- return name and formatters["%s: \\%s, slot: %i"](what,name,slot) or ""
461        return name and formatters["%s \\%s"](what,name) or ""
462    end, "provide lua call details")
463
464end
465
466setmetatablenewindex(interfacescanners, function(t,k,v)
467    report_cld("don't register scanner %a directly",k)
468 -- registerscanner(k,v)
469end)
470
471interfaces.scanners = storage.mark(interfacescanners)
472
473context.functions = {
474    register   = function(qualifiedname) return registerfunction(qualifiedname) end, -- only one argument
475    unregister = unregisterfunction,
476    reserve    = reservefunction,
477    known      = knownfunctions,
478    callonce   = callfunctiononce,
479}
480
481function commands.ctxfunction(code,namespace)
482    context(registerfunction(code,namespace))
483end
484
485function commands.ctxscanner(name,code,namespace)
486    local n = registerfunction(code,namespace)
487    if storedscanners[name] then
488        storedscanners[name] = n
489    end
490    context(n)
491end
492
493local function dummy() end
494
495local texsetmacro = token.setmacro or token.set_macro
496
497function commands.ctxresetter(name) -- to be checked
498    return function()
499        if storedscanners[name] then
500            rawset(interfacescanners,name,dummy)
501         -- context.resetctxscanner(privatenamespace .. name)
502            texsetmacro(privatenamespace .. name,"","global")
503        end
504    end
505end
506
507-- Should we keep the catcodes with the function?
508
509local catcodestack    = { }
510local currentcatcodes = ctxcatcodes
511local contentcatcodes = ctxcatcodes
512
513local catcodes = {
514    ctx = ctxcatcodes, ctxcatcodes = ctxcatcodes, context  = ctxcatcodes,
515    prt = prtcatcodes, prtcatcodes = prtcatcodes, protect  = prtcatcodes,
516    tex = texcatcodes, texcatcodes = texcatcodes, plain    = texcatcodes,
517    txt = txtcatcodes, txtcatcodes = txtcatcodes, text     = txtcatcodes,
518    vrb = vrbcatcodes, vrbcatcodes = vrbcatcodes, verbatim = vrbcatcodes,
519    xml = xmlcatcodes, xmlcatcodes = xmlcatcodes,
520}
521
522-- maybe just increment / decrement
523
524-- local function pushcatcodes(c)
525--     insert(catcodestack,currentcatcodes)
526--     currentcatcodes = (c and catcodes[c] or tonumber(c)) or currentcatcodes
527--     contentcatcodes = currentcatcodes
528-- end
529--
530-- local function popcatcodes()
531--     currentcatcodes = remove(catcodestack) or currentcatcodes
532--     contentcatcodes = currentcatcodes
533-- end
534
535local catcodelevel = 0
536
537local function pushcatcodes(c)
538    catcodelevel = catcodelevel + 1
539    catcodestack[catcodelevel] = currentcatcodes
540    currentcatcodes = (c and catcodes[c] or tonumber(c)) or currentcatcodes
541    contentcatcodes = currentcatcodes
542end
543
544local function popcatcodes()
545    if catcodelevel > 0 then
546        currentcatcodes = catcodestack[catcodelevel] or currentcatcodes
547        catcodelevel = catcodelevel - 1
548    end
549    contentcatcodes = currentcatcodes
550end
551
552function context.unprotect()
553    -- at the lua end
554    catcodelevel = catcodelevel + 1
555    catcodestack[catcodelevel] = currentcatcodes
556    currentcatcodes = prtcatcodes
557    contentcatcodes = prtcatcodes
558    -- at the tex end
559    flush("\\unprotect")
560end
561
562function context.protect()
563    -- at the tex end
564    flush("\\protect")
565    -- at the lua end
566    if catcodelevel > 0 then
567        currentcatcodes = catcodestack[catcodelevel] or currentcatcodes
568        catcodelevel = catcodelevel - 1
569    end
570    contentcatcodes = currentcatcodes
571end
572
573context.catcodes     = catcodes
574context.pushcatcodes = pushcatcodes
575context.popcatcodes  = popcatcodes
576
577-- -- --
578
579local newline       = patterns.newline
580local space         = patterns.spacer
581local spacing       = newline * space^0
582local content       = lpegC((1-spacing)^1)            -- texsprint
583local emptyline     = space^0 * newline^2             -- texprint("")
584                    + newline * space^1 * newline^1
585local endofline     = space^0 * newline * space^0     -- texsprint(" ")
586local simpleline    = endofline * lpegP(-1)           --
587
588local verbose       = lpegC((1-space-newline)^1)
589local beginstripper = (lpegS(" \t")^1 * newline^1) / ""
590local endstripper   = beginstripper * lpegP(-1)
591
592local justaspace    = space * lpegCc("")
593local justanewline  = newline * lpegCc("")
594
595local function n_content   (s) flush      (contentcatcodes,s    ) end
596local function n_verbose   (s) flush      (vrbcatcodes,    s    ) end
597local function n_endofline ()  flush      (currentcatcodes," \r") end
598local function n_emptyline ()  flushdirect(currentcatcodes,"\r" ) end
599local function n_simpleline()  flush      (currentcatcodes," \r") end
600
601local n_exception = ""
602
603-- better a table specification
604
605function context.newtexthandler(specification)
606    specification = specification or { }
607    --
608    local s_catcodes   = specification.catcodes
609    --
610    local f_before     = specification.before
611    local f_after      = specification.after
612    --
613    local f_endofline  = specification.endofline  or n_endofline
614    local f_emptyline  = specification.emptyline  or n_emptyline
615    local f_simpleline = specification.simpleline or n_simpleline
616    local f_content    = specification.content    or n_content
617    local f_space      = specification.space
618    --
619    local p_exception  = specification.exception
620    --
621    if s_catcodes then
622        f_content = function(s)
623            flush(s_catcodes,s)
624        end
625    end
626    --
627    local pattern
628    if f_space then
629        if p_exception then
630            local content = lpegC((1-spacing-p_exception)^1)
631            pattern =
632              (
633                    justaspace   / f_space
634                  + justanewline / f_endofline
635                  + p_exception
636                  + content      / f_content
637                )^0
638        else
639            local content = lpegC((1-space-endofline)^1)
640            pattern =
641                (
642                    justaspace   / f_space
643                  + justanewline / f_endofline
644                  + content      / f_content
645                )^0
646        end
647    else
648        if p_exception then
649            local content = lpegC((1-spacing-p_exception)^1)
650            pattern =
651                simpleline / f_simpleline
652              +
653              (
654                    emptyline  / f_emptyline
655                  + endofline  / f_endofline
656                  + p_exception
657                  + content    / f_content
658                )^0
659        else
660            local content = lpegC((1-spacing)^1)
661            pattern =
662                simpleline / f_simpleline
663                +
664                (
665                    emptyline / f_emptyline
666                  + endofline / f_endofline
667                  + content   / f_content
668                )^0
669        end
670    end
671    --
672    if f_before then
673        pattern = (P(true) / f_before) * pattern
674    end
675    --
676    if f_after then
677        pattern = pattern * (P(true) / f_after)
678    end
679    --
680    return function(str) return lpegmatch(pattern,str) end, pattern
681end
682
683function context.newverbosehandler(specification) -- a special variant for e.g. cdata in lxml-tex
684    specification = specification or { }
685    --
686    local f_line    = specification.line    or function() flushdirect("\r") end
687    local f_space   = specification.space   or function() flush      (" ")  end
688    local f_content = specification.content or n_verbose
689    local f_before  = specification.before
690    local f_after   = specification.after
691    --
692    local pattern =
693        justanewline / f_line    -- so we get call{}
694      + verbose      / f_content
695      + justaspace   / f_space   -- so we get call{}
696    --
697    if specification.strip then
698        pattern = beginstripper^0 * (endstripper + pattern)^0
699    else
700        pattern = pattern^0
701    end
702    --
703    if f_before then
704        pattern = (lpegP(true) / f_before) * pattern
705    end
706    --
707    if f_after then
708        pattern = pattern * (lpegP(true) / f_after)
709    end
710    --
711    return function(str) return lpegmatch(pattern,str) end, pattern
712end
713
714local flushlines = context.newtexthandler {
715    content    = n_content,
716    endofline  = n_endofline,
717    emptyline  = n_emptyline,
718    simpleline = n_simpleline,
719}
720
721-- The next variant is only used in rare cases (buffer to mp):
722
723local printlines_ctx = (
724    (newline)     / function()  texprint("") end +
725    (1-newline)^1 / function(s) texprint(ctxcatcodes,s) end * newline^-1
726)^0
727
728local printlines_raw = (
729    (newline)     / function()  texprint("") end +
730    (1-newline)^1 / function(s) texprint(s)  end * newline^-1
731)^0
732
733function context.printlines(str,raw)     -- todo: see if via file is useable
734    if raw then
735        lpegmatch(printlines_raw,str)
736    else
737        lpegmatch(printlines_ctx,str)
738    end
739end
740
741function context.printtable(t,separator)     -- todo: see if via file is useable
742    if separator == nil or separator == true then
743        separator = "\r"
744    elseif separator == "" or separator == false then
745        separator = ""
746    end
747    local s = concat(t,separator)
748    if s ~= "" then
749        context(s)
750    end
751end
752
753-- -- -- "{" .. ti .. "}" is somewhat slower in a cld-mkiv run than "{",ti,"}"
754
755local containseol = patterns.containseol
756
757local lua_call_code = tokens.commands.lua_expandable_call or tokens.commands.lua_call
758
759local sortedhashindeed = false
760
761directives.register("context.sorthash",function(v)
762    sortedhashindeed = v and table.sortedhash or nil
763end)
764
765local function writer(parent,command,...) -- already optimized before call
766    if type(command) == "string" then -- for now
767        flush(currentcatcodes,command) -- todo: ctx|prt|texcatcodes
768    else
769        flush(command) -- todo: ctx|prt|texcatcodes
770    end
771    local direct = false
772    for i=1,select("#",...) do
773        local ti = select(i,...)
774        if direct then
775            local typ = type(ti)
776            if typ == "string" or typ == "number" then
777                flush(currentcatcodes,ti)
778            else -- node.write
779                report_context("error: invalid use of direct in %a, only strings and numbers can be flushed directly, not %a",command,typ)
780            end
781            direct = false
782        elseif ti == nil then
783            -- nothing
784        elseif ti == "" then
785            flush(currentcatcodes,"{}")
786     -- elseif ti == 1 then
787     --     flush(currentcatcodes,"{1}")
788        else
789            local typ = type(ti)
790            if typ == "string" then
791                -- is processlines seen ?
792                if processlines and lpegmatch(containseol,ti) then
793                    flush(currentcatcodes,"{")
794                    flushlines(ti)
795                    flush(currentcatcodes,"}")
796                elseif currentcatcodes == contentcatcodes then
797                    flush(currentcatcodes,"{",ti,"}")
798                else
799                    flush(currentcatcodes,"{")
800                    flush(contentcatcodes,ti)
801                    flush(currentcatcodes,"}")
802                end
803            elseif typ == "number" then
804                -- numbers never have funny catcodes
805                flush(currentcatcodes,"{",ti,"}")
806            elseif typ == "table" then
807                local tn = #ti
808                if tn == 0 then
809                    local done = false
810                    if sortedhashindeed then
811                        for k, v in sortedhashindeed(ti) do
812                            if done then
813                                if v == "" then
814                                    flush(currentcatcodes,",",k,'=')
815                                else
816                                    flush(currentcatcodes,",",k,"={",v,"}")
817                                end
818                            else
819                                if v == "" then
820                                    flush(currentcatcodes,"[",k,"=")
821                                else
822                                    flush(currentcatcodes,"[",k,"={",v,"}")
823                                end
824                                done = true
825                            end
826                        end
827                    else
828                        for k, v in next, ti do
829                            if done then
830                                if v == "" then
831                                    flush(currentcatcodes,",",k,'=')
832                                else
833                                    flush(currentcatcodes,",",k,"={",v,"}")
834                                end
835                            else
836                                if v == "" then
837                                    flush(currentcatcodes,"[",k,"=")
838                                else
839                                    flush(currentcatcodes,"[",k,"={",v,"}")
840                                end
841                                done = true
842                            end
843                        end
844                    end
845                    if done then
846                        flush(currentcatcodes,"]")
847                    else
848                        flush(currentcatcodes,"[]")
849                    end
850                elseif tn == 1 then -- some 20% faster than the next loop
851                    local tj = ti[1]
852                    if type(tj) == "function" then
853                        tj = storefunction(tj)
854                        flush(currentcatcodes,"[",newtoken(tj,lua_call_code),"]")
855                    else
856                        flush(currentcatcodes,"[",tj,"]")
857                    end
858                else
859                    flush(currentcatcodes,"[")
860                    for j=1,tn do
861                        local tj = ti[j]
862                        if type(tj) == "function" then
863                            tj = storefunction(tj)
864                            flush(currentcatcodes,"[",newtoken(tj,lua_call_code),j == tn and "]" or ",")
865                        else
866                            if j == tn then
867                                flush(currentcatcodes,tj,"]")
868                            else
869                                flush(currentcatcodes,tj,",")
870                            end
871                        end
872                    end
873                end
874            elseif typ == "function" then
875                -- todo: ctx|prt|texcatcodes
876                ti = storefunction(ti)
877                flush(currentcatcodes,"{",newtoken(ti,lua_call_code),"}")
878            elseif typ == "boolean" then
879                if ti then
880                    flushdirect(currentcatcodes,"\r")
881                else
882                    direct = true
883                end
884            elseif typ == "thread" then
885                report_context("coroutines not supported as we cannot yield across boundaries")
886         -- elseif isnode(ti) or istoken(ti) then
887            elseif isprintable(ti) then
888                flush(currentcatcodes,"{",ti,"}")
889            else
890                local s = tostring(ti)
891                if s then
892                    flushdirect(currentcatcodes,s)
893                else
894                    report_context("error: %a gets a weird argument %a",command,ti)
895                end
896            end
897     -- else
898     --     local n = isnode(ti)
899     --     if n then
900     --         flush(ti)
901     --     else
902     --         report_context("error: %a gets a weird argument %a",command,ti)
903     --     end
904        end
905    end
906end
907
908local toks = tokens.cache
909context.tokenizedcs = toks
910
911local core = setmetatableindex(function(parent,k)
912    local t
913    local f = function(first,...)
914        if not t then
915            t = toks[k]
916        end
917        if first == nil then
918            flush(t)
919        else
920            return writer(context,t,first,...)
921        end
922    end
923    parent[k] = f
924    return f
925end)
926
927core.cs = setmetatableindex(function(parent,k)
928    local t
929    local f = function()
930        if not t then
931            t = toks[k]
932        end
933        flush(t)
934    end
935    parent[k] = f
936    return f
937end)
938
939local indexer = function(parent,k)
940    if type(k) == "string" then
941        return core[k]
942    else
943        return context -- catch
944    end
945end
946
947context.core = core
948
949-- only for internal usage:
950
951-- local prtindexer = nil
952--
953-- do
954--
955--     -- the only variant is not much faster than the full but it's more
956--     -- memory efficient
957--
958--     local protected     = { }
959--     local protectedcs   = { }
960--     context.protected   = protected
961--     context.protectedcs = protectedcs
962--
963--     local function fullindexer(t,k)
964--         local c = "\\" .. k -- tostring(k)
965--         local v = function(first,...)
966--             if first == nil then
967--                 flush(prtcatcodes,c)
968--             else
969--                 return prtwriter(c,first,...)
970--             end
971--         end
972--         rawset(t,k,v) -- protected namespace
973--         return v
974--     end
975--
976--     local function onlyindexer(t,k)
977--         local c = "\\" .. k -- tostring(k)
978--         local v = function()
979--             flush(prtcatcodes,c)
980--         end
981--         rawset(protected,k,v)
982--         rawset(t,k,v)
983--         return v
984--     end
985--
986--     protected.cs = setmetatableindex(function(parent,k)
987--         local c = "\\" .. k -- tostring(k)
988--         local v = function()
989--             flush(prtcatcodes,c)
990--         end
991--         parent[k] = v
992--         return v
993--     end
994--
995--     setmetatableindex(protected,fullindexer)
996--     setmetatablecall (protected,prtwriter)
997--
998--     setmetatableindex(protectedcs,onlyindexer)
999--     setmetatablecall (protectedcs,prtwriter)
1000--
1001-- end
1002
1003-- local splitformatters = utilities.strings.formatters.new(true) -- not faster (yet)
1004
1005local caller = function(parent,f,a,...)
1006    if not parent then
1007        -- so we don't need to test in the calling (slower but often no issue)
1008    elseif f ~= nil then
1009        local typ = type(f)
1010        if typ == "string" then
1011            if f == "" then
1012                -- new, can save a bit sometimes
1013             -- if trace_context then
1014             --     report_context("empty argument to context()")
1015             -- end
1016            elseif a then
1017                flush(contentcatcodes,formatters[f](a,...)) -- was currentcatcodes
1018             -- flush(contentcatcodes,splitformatters[f](a,...)) -- was currentcatcodes
1019            elseif processlines and lpegmatch(containseol,f) then
1020                flushlines(f)
1021            else
1022                flush(contentcatcodes,f)
1023            end
1024        elseif typ == "number" then
1025            if a then
1026                flush(currentcatcodes,f,a,...)
1027            else
1028                flush(currentcatcodes,f)
1029            end
1030        elseif typ == "function" then
1031            -- ignored: a ...
1032            f = storefunction(f)
1033            flush(currentcatcodes,"{",newtoken(f,lua_call_code),"}")
1034        elseif typ == "boolean" then
1035            if f then
1036                if a ~= nil then
1037                    flushlines(a)
1038                else
1039                    flushdirect(currentcatcodes,"\n") -- no \r, else issues with \startlines ... use context.par() otherwise
1040                end
1041            else
1042                if a ~= nil then
1043                    -- no command, same as context(a,...)
1044                    writer(parent,"",a,...)
1045                else
1046                    -- ignored
1047                end
1048            end
1049        elseif typ == "thread" then
1050            report_context("coroutines not supported as we cannot yield across boundaries")
1051     -- elseif isnode(f) or istoken(f) then
1052        elseif isprintable(f) then
1053            flush(f)
1054        else
1055            local s = tostring(f)
1056            if s then
1057                flushdirect(currentcatcodes,s)
1058            else
1059                report_context("error: %a gets a weird argument %a","context",f)
1060            end
1061        end
1062 -- else
1063 --     local n = isnode(f)
1064 --     if n then
1065 --         flush(f)
1066 --     else
1067 --         report_context("error: %a gets a weird argument %a","context",f)
1068 --     end
1069    end
1070end
1071
1072context.nodes = { -- todo
1073    store = storenode,
1074    flush = function(n)
1075        flush(n)
1076    end,
1077}
1078
1079context.nuts = { -- todo
1080    store = function(n)
1081        return storenode(tonut(n))
1082    end,
1083    flush = function(n,d)
1084        flush(tonode(n))
1085    end,
1086}
1087
1088local defaultcaller = caller
1089
1090setmetatableindex(context,indexer)
1091setmetatablecall (context,caller)
1092
1093function  context.sprint(...) -- takes catcodes as first argument
1094    flush(...)
1095end
1096
1097function context.fprint(first,second,third,...)
1098    if type(first) == "number" then
1099        if third then
1100            flush(first,formatters[second](third,...))
1101        else
1102            flush(first,second)
1103        end
1104    else
1105        if second then
1106            flush(formatters[first](second,third,...))
1107        else
1108            flush(first)
1109        end
1110    end
1111end
1112
1113tex.fprint = context.fprint
1114
1115-- logging
1116
1117local trace_stack       = { report_context }
1118
1119local normalflush       = flush
1120local normalflushdirect = flushdirect
1121----- normalflushraw    = flushraw
1122local normalwriter      = writer
1123local currenttrace      = report_context
1124local nofwriters        = 0
1125local nofflushes        = 0
1126
1127local tracingpermitted  = true
1128
1129local visualizer = lpeg.replacer {
1130    { "\n", "<<newline>>" },
1131    { "\r", "<<par>>" },
1132}
1133
1134statistics.register("traced context", function()
1135    local used, freed = usedstack()
1136    local unreachable = used - freed
1137    if nofwriters > 0 or nofflushes > 0 then
1138        return format("writers: %s, flushes: %s, maxstack: %s",nofwriters,nofflushes,used,freed,unreachable)
1139    elseif showstackusage or unreachable > 0 then
1140        return format("maxstack: %s, freed: %s, unreachable: %s",used,freed,unreachable)
1141    end
1142end)
1143
1144-- The cmd names were synchronized with the normal call cmd names.
1145
1146local luacalls = {
1147    lua_function_call  = true,
1148    lua_protected_call = true,
1149    lua_value          = true,
1150    lua_local_call     = true,
1151    lua_call           = true,
1152}
1153
1154local function userdata(argument)
1155    if isnode(argument) then
1156        return formatters["<< %s node %i>>"](nodes.nodecodes[argument.id],tonut(argument))
1157    elseif istoken(argument) then
1158        local csname = argument.csname
1159        if csname then
1160         -- return formatters["<<\\%s>>"](csname)
1161            return formatters["\\%s"](csname)
1162        end
1163        if luacalls[argument.cmdname] then
1164            return "<<function>>" -- argument.mode
1165        end
1166        return "<<token>>"
1167    else
1168        return "<<userdata>>"
1169    end
1170end
1171
1172
1173local tracedwriter = function(parent,...) -- also catcodes ?
1174    nofwriters = nofwriters + 1
1175    local savedflush       = flush
1176    local savedflushdirect = flushdirect -- unlikely to be used here
1177    local t = { "w : - : " }
1178    local n = 1
1179    local traced = function(catcodes,...) -- todo: check for catcodes
1180        local s = type(catcodes) == "number" and { ... } or { catcodes, ... }
1181        for i=1,#s do
1182            local argument = s[i]
1183            local argtype  = type(argument)
1184            if argtype == "string" then
1185                s[i] = lpegmatch(visualizer,argument)
1186            elseif argtype == "number" then
1187                s[i] = argument
1188            elseif argtype == "userdata" then
1189                s[i] = userdata(argument)
1190            else
1191                s[i] = formatters["<<%S>>"](argument)
1192            end
1193        end
1194        s = concat(s)
1195        s = lpegmatch(visualizer,s)
1196        n = n + 1
1197        t[n] = s
1198    end
1199    flush = function(...)
1200        normalflush(...)
1201        if tracingpermitted then
1202            traced(...)
1203        end
1204    end
1205    flushdirect = function(...)
1206        normalflushdirect(...)
1207        if tracingpermitted then
1208            traced(...)
1209        end
1210    end
1211    normalwriter(parent,...)
1212    flush       = savedflush
1213    flushdirect = savedflushdirect
1214    currenttrace(concat(t))
1215end
1216
1217-- we could reuse collapsed
1218
1219local traced = function(one,two,...)
1220    if two ~= nil then
1221        -- only catcodes if 'one' is number
1222        local catcodes  = type(one) == "number" and one
1223        local arguments = catcodes and { two, ... } or { one, two, ... }
1224        local collapsed = { formatters["f : %s : "](catcodes or '-') }
1225        local c         = 1
1226        for i=1,#arguments do
1227            local argument = arguments[i]
1228            local argtype = type(argument)
1229            c = c + 1
1230            if argtype == "string" then
1231                collapsed[c] = lpegmatch(visualizer,argument)
1232            elseif argtype == "number" then
1233                collapsed[c] = argument
1234            elseif argtype == "userdata" then
1235                collapsed[c] = userdata(argument)
1236            else
1237                collapsed[c] = formatters["<<%S>>"](argument)
1238            end
1239        end
1240        currenttrace(concat(collapsed))
1241    elseif one ~= nil then
1242        -- no catcodes
1243        local argtype = type(one)
1244        if argtype == "string" then
1245            currenttrace(formatters["f : - : %s"](lpegmatch(visualizer,one)))
1246        elseif argtype == "number" then
1247            currenttrace(formatters["f : - : %s"](one))
1248        elseif argtype == "userdata" then
1249            currenttrace(formatters["F : - : %s"](userdata(one)))
1250        else
1251            currenttrace(formatters["f : - : <<%S>>"](one))
1252        end
1253    end
1254end
1255
1256local tracedflush = function(one,two,...)
1257    nofflushes = nofflushes + 1
1258    if two ~= nil then
1259        normalflush(one,two,...)
1260    else
1261        normalflush(one)
1262    end
1263    if tracingpermitted then
1264        traced(one,two,...)
1265    end
1266end
1267
1268local tracedflushdirect = function(one,two,...)
1269    nofflushes = nofflushes + 1
1270    if two ~= nil then
1271        normalflushdirect(one,two,...)
1272    else
1273        normalflushdirect(one)
1274    end
1275    if tracingpermitted then
1276        traced(one,two,...)
1277    end
1278end
1279
1280function context.pushlogger(trace)
1281    trace = trace or report_context
1282    insert(trace_stack,currenttrace)
1283    currenttrace = trace
1284end
1285
1286function context.poplogger()
1287    if #trace_stack > 1 then
1288        currenttrace = remove(trace_stack) or report_context
1289    else
1290        currenttrace = report_context
1291    end
1292end
1293
1294function context.settracing(v)
1295    if v then
1296        flush       = tracedflush
1297        flushdirect = tracedflushdirect
1298        writer      = tracedwriter
1299    else
1300        flush       = normalflush
1301        flushdirect = normalflushdirect
1302        writer      = normalwriter
1303    end
1304    return flush, writer, flushdirect
1305end
1306
1307function context.getlogger()
1308    return flush, writer, flushdirect
1309end
1310
1311trackers.register("context.trace",context.settracing)
1312
1313local trace_cld = false  trackers.register("context.files", function(v) trace_cld = v end)
1314
1315do
1316
1317    -- This is the most reliable way to deal with nested buffers and other
1318    -- catcode sensitive data.
1319
1320    local resolve     = resolvers.savers.byscheme
1321    local validstring = string.valid
1322    local input       = context.input
1323
1324    local function viafile(data,tag)
1325        if data and data ~= "" then
1326            local filename = resolve("virtual",validstring(tag,"viafile"),data)
1327         -- context.startregime { "utf" }
1328            input(filename)
1329         -- context.stopregime()
1330        end
1331    end
1332
1333    context.viafile    = viafile
1334
1335    -- experiment for xtables, don't use it elsewhere yet
1336
1337    local collected    = nil
1338    local nofcollected = 0
1339    local sentinel     = string.char(26) -- ASCII SUB character : endoffileasciicode : ignorecatcode
1340    local level        = 0
1341
1342    local function collect(c,a,...) -- can be optimized
1343        if type(c) == "userdata" then
1344            nofcollected = nofcollected + 1
1345         -- collected[nofcollected] = userdata(c)
1346            collected[nofcollected] = "\\" .. c.csname
1347        end
1348        if a then
1349            for i=1,select("#",a,...) do
1350                local c = select(i,a,...)
1351                nofcollected = nofcollected + 1
1352                collected[nofcollected] = type(c) == "userdata" and userdata(c) or c
1353            end
1354        end
1355    end
1356
1357    local collectdirect = collect
1358    local permitted     = true
1359
1360    -- doesn't work well with tracing do we need to avoid that when
1361    -- collecting stuff
1362
1363    function context.startcollecting()
1364        if level == 0 then
1365            collected    = { }
1366            nofcollected = 0
1367            flush        = collect
1368            flushdirect  = collectdirect
1369            permitted    = tracingpermitted
1370        end
1371        level = level + 1
1372    end
1373
1374    function context.stopcollecting()
1375        level = level - 1
1376        if level < 1 then
1377            local result     = concat(collected,sentinel)
1378            flush            = normalflush
1379            flushdirect      = normalflushdirect
1380            tracingpermitted = permitted
1381            collected        = nil
1382            nofcollected     = 0
1383            level            = 0
1384            viafile(result)
1385        end
1386    end
1387
1388    local findtexfile = resolvers.findtexfile
1389    local findfile    = resolvers.findfile
1390
1391    function context.runfile(filename)
1392        local foundname = findtexfile(file.addsuffix(filename,"cld")) or ""
1393        if foundname ~= "" then
1394            local ok = dofile(foundname)
1395            if type(ok) == "function" then
1396                if trace_cld then
1397                    report_context("begin of file %a (function call)",foundname)
1398                end
1399                ok()
1400                if trace_cld then
1401                    report_context("end of file %a (function call)",foundname)
1402                end
1403            elseif ok then
1404                report_context("file %a is processed and returns true",foundname)
1405            else
1406                report_context("file %a is processed and returns nothing",foundname)
1407            end
1408        else
1409            report_context("unknown file %a",filename)
1410        end
1411    end
1412
1413    function context.loadfile(filename)
1414        context(stripstring(loaddata(findfile(filename))))
1415    end
1416
1417    function context.loadviafile(filename)
1418        viafile(stripstring(loaddata(findfile(filename))))
1419    end
1420
1421end
1422
1423-- some functions
1424
1425function context.direct(first,...)
1426    if first ~= nil then
1427        return writer(context,"",first,...)
1428    end
1429end
1430
1431-- context.delayed (todo: lines)
1432
1433do
1434
1435    local delayed = { }
1436
1437    local function indexer(parent,k)
1438        local f = function(...)
1439            local a = { ... } -- this also freezes ...
1440            return function()
1441             -- return context[k](unpack(a))
1442                return core[k](unpack(a))
1443            end
1444        end
1445        parent[k] = f
1446        return f
1447    end
1448
1449    local function caller(parent,...) -- todo: nodes
1450        local a = { ... }
1451        return function()
1452         -- return context(unpack(a))
1453            return defaultcaller(context,unpack(a))
1454        end
1455    end
1456
1457    setmetatableindex(delayed,indexer)
1458    setmetatablecall (delayed,caller)
1459
1460    context.delayed = delayed
1461
1462end
1463
1464-- do
1465--
1466--     -- context.nested (todo: lines), creates strings
1467--
1468--     local nested = { }
1469--
1470--     local function indexer(parent,k) -- not ok when traced
1471--         local f = function(...)
1472--             local t, savedflush, n = { }, flush, 0
1473--             flush = function(c,f,s,...) -- catcodes are ignored
1474--                 n = n + 1
1475--                 t[n] = s and concat{f,s,...} or f -- optimized for #args == 1
1476--             end
1477--          -- context[k](...)
1478--             core[k](...)
1479--             flush = savedflush
1480--             return concat(t)
1481--         end
1482--         parent[k] = f
1483--         return f
1484--     end
1485--
1486--     local function caller(parent,...)
1487--         local t, savedflush, n = { }, flush, 0
1488--         flush = function(c,f,s,...) -- catcodes are ignored
1489--             n = n + 1
1490--             t[n] = s and concat{f,s,...} or f -- optimized for #args == 1
1491--         end
1492--      -- context(...)
1493--         defaultcaller(context,...)
1494--         flush = savedflush
1495--         return concat(t)
1496--     end
1497--
1498--     setmetatableindex(nested,indexer)
1499--     setmetatablecall (nested,caller)
1500--
1501--     context.nested = nested
1502--
1503-- end
1504
1505context.nested = context.delayed
1506
1507-- verbatim
1508
1509function context.newindexer(catcodes,cmdcodes)
1510    local handler = { }
1511
1512    local function indexer(parent,k)
1513        local command = core[k]
1514        local f = function(...)
1515            local savedcatcodes = contentcatcodes
1516            contentcatcodes = catcodes
1517            command(...)
1518            contentcatcodes = savedcatcodes
1519        end
1520        parent[k] = f
1521        return f
1522    end
1523
1524    local function caller(parent,...)
1525        local savedcatcodes = contentcatcodes
1526        contentcatcodes = catcodes
1527        defaultcaller(parent,...)
1528        contentcatcodes = savedcatcodes
1529    end
1530
1531    handler.cs = core.cs
1532
1533    setmetatableindex(handler,indexer)
1534    setmetatablecall (handler,caller)
1535
1536    return handler
1537end
1538
1539context.verbatim  = context.newindexer(vrbcatcodes,ctxcatcodes)
1540context.puretext  = context.newindexer(txtcatcodes,ctxcatcodes)
1541context.protected = context.newindexer(prtcatcodes,prtcatcodes)
1542
1543-- formatted
1544
1545do
1546
1547    local formatted = { }
1548
1549    -- formatted.command([catcodes,]format[,...])
1550
1551    local function formattedflush(parent,c,catcodes,fmt,...)
1552        if not catcodes then
1553            return writer(parent,c)
1554        elseif not fmt then
1555            return writer(parent,c,catcodes)
1556        elseif type(catcodes) == "number" then
1557            local result
1558            pushcatcodes(catcodes)
1559            result = writer(parent,c,formatters[fmt](...))
1560            popcatcodes()
1561            return result
1562        else
1563            return writer(parent,c,formatters[catcodes](fmt,...))
1564        end
1565    end
1566
1567    local toks = tokens.cache
1568
1569    local indexer = function(parent,k)
1570        if type(k) == "string" then
1571            local t
1572            local f = function(first,...)
1573                if not t then
1574                    t = toks[k]
1575                end
1576                if first == nil then
1577                    flush(t)
1578                else
1579                    return formattedflush(parent,t,first,...)
1580                end
1581            end
1582            parent[k] = f
1583            return f
1584        else
1585            return context -- catch
1586        end
1587    end
1588
1589    -- formatted([catcodes,]format[,...])
1590
1591    local function caller(parent,catcodes,fmt,...)
1592        if not catcodes then
1593            -- nothing
1594        elseif not fmt then
1595            flush(catcodes)
1596        elseif type(catcodes) == "number" then
1597            flush(catcodes,formatters[fmt](...))
1598        else
1599            flush(formatters[catcodes](fmt,...))
1600        end
1601    end
1602
1603    setmetatableindex(formatted,indexer)
1604    setmetatablecall (formatted,caller)
1605
1606    context.formatted = formatted
1607
1608end
1609
1610do
1611
1612    -- metafun (this will move to another file)
1613
1614    local metafun = { }
1615
1616    function metafun.start()
1617        context.startMPcode()
1618    end
1619
1620    function metafun.stop()
1621        context.stopMPcode()
1622    end
1623
1624    setmetatablecall(metafun,defaultcaller)
1625
1626    function metafun.color(name) -- obsolete
1627        return name -- formatters[ [[\MPcolor{%s}]] ](name)
1628    end
1629
1630    -- metafun.delayed
1631
1632    local delayed = { }
1633
1634    local function indexer(parent,k)
1635        local f = function(...)
1636            local a = { ... }
1637            return function()
1638                return metafun[k](unpack(a))
1639            end
1640        end
1641        parent[k] = f
1642        return f
1643    end
1644
1645
1646    local function caller(parent,...)
1647        local a = { ... }
1648        return function()
1649            return metafun(unpack(a))
1650        end
1651    end
1652
1653    setmetatableindex(delayed,indexer)
1654    setmetatablecall (delayed,caller)
1655
1656    context.metafun = metafun
1657    metafun.delayed = delayed
1658
1659end
1660
1661-- helpers:
1662
1663do
1664
1665    function context.concat(...)
1666        context(concat(...))
1667    end
1668
1669    local p_texescape = patterns.texescape
1670
1671    function context.escaped(s)
1672        if s then
1673            context(lpegmatch(p_texescape,s) or s)
1674        else
1675         -- context("")
1676        end
1677    end
1678
1679    function context.escape(s)
1680        if s then
1681            return lpegmatch(p_texescape,s) or s
1682        else
1683            return ""
1684        end
1685    end
1686
1687end
1688
1689-- templates
1690
1691do
1692
1693    local single  = lpegP("%")
1694    local double  = lpegP("%%")
1695    local lquoted = lpegP("%[")
1696    local rquoted = lpegP("]%")
1697    local space   = lpegP(" ")
1698
1699    local start = [[
1700    local texescape = lpeg.patterns.texescape
1701    local lpegmatch = lpeg.match
1702    return function(variables) return
1703    ]]
1704
1705    local stop  = [[
1706    end
1707    ]]
1708
1709    local replacer = lpegP { "parser",
1710        parser   = lpegCs(lpegCc(start) * lpegV("step") * (lpegCc("..") * lpegV("step"))^0 * lpegCc(stop)),
1711        unquoted = (lquoted*space/'')
1712                 * ((lpegC((1-space*rquoted)^1)) / "lpegmatch(texescape,variables%0 or '')" )
1713                 * (space*rquoted/'')
1714                 + (lquoted/'')
1715                 * ((lpegC((1-rquoted)^1)) / "lpegmatch(texescape,variables['%0'] or '')" )
1716                 * (rquoted/''),
1717        key      = (single*space/'')
1718                 * ((lpegC((1-space*single)^1)) / "(variables%0 or '')" )
1719                 * (space*single/'')
1720                 + (single/'')
1721                 * ((lpegC((1-single)^1)) / "(variables['%0'] or '')" )
1722                 * (single/''),
1723        escape   = double/'%%',
1724        step     = lpegV("unquoted")
1725                 + lpegV("escape")
1726                 + lpegV("key")
1727                 + lpegCc("\n[===[") * (1 - lpegV("unquoted") - lpegV("escape") - lpegV("key"))^1 * lpegCc("]===]\n"),
1728    }
1729
1730    local templates = { }
1731
1732    local function indexer(parent,k)
1733        local v = lpegmatch(replacer,k)
1734        if not v then
1735         -- report_template("invalid template:\n%s",k)
1736            v = "error: no valid template (1)"
1737        else
1738            local f = loadstring(v)
1739            if type(f) ~= "function" then
1740             -- report_template("invalid template:\n%s\n=>\n%s",k,v)
1741                v = "error: no valid template (2)"
1742            else
1743                f = f()
1744                if not f then
1745                 -- report_template("invalid template:\n%s\n=>\n%s",k,v)
1746                    v = "error: no valid template (3)"
1747                else
1748                    v = f
1749                end
1750            end
1751        end
1752        if type(v) == "function" then
1753            local f = function(first,second)
1754                if second then
1755                    pushcatcodes(first)
1756                    flushlines(v(second))
1757                    popcatcodes()
1758                else
1759                    flushlines(v(first))
1760                end
1761            end
1762            parent[k] = f
1763            return f
1764        else
1765            return function()
1766                flush(v)
1767            end
1768        end
1769
1770    end
1771
1772    local function caller(parent,k,...)
1773        return parent[k](...)
1774    end
1775
1776    setmetatableindex(templates,indexer)
1777    setmetatablecall (templates,caller)
1778
1779    context.templates = templates
1780
1781end
1782
1783-- The above is a bit over the top as we could also stick to a simple context.replace
1784-- which is fast enough anyway, but the above fits in nicer, also with the catcodes.
1785--
1786-- local replace = utilities.templates.replace
1787--
1788-- function context.template(template,variables)
1789--     context(replace(template,variables))
1790-- end
1791
1792do
1793
1794    -- not the best namespace -- some day maybe texs a la nodes and tokens .. also,
1795    -- we already have tex.modes so we need a different name
1796
1797    local modelevels = tex.getmodevalues()
1798    local t          = table.keys(modelevels)
1799    tex.modelevels   = table.swapped(modelevels,modelevels) -- utilities.storage.allocate()
1800
1801    for i=1,#t do local k = t[i] modelevels[-k] = modelevels[k] end
1802
1803    if CONTEXTLMTXMODE > 0 then
1804
1805        -- also elsewhere
1806
1807        local flagcodes = tex.getflagvalues()
1808        tex.flagcodes   = table.swapped(flagcodes,flagcodes) -- utilities.storage.allocate()
1809
1810    end
1811
1812end
1813