buff-ini.lmt /size: 28 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['buff-ini'] = {
2    version   = 1.001,
3    comment   = "companion to buff-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
9local concat = table.concat
10local type, next, load = type, next, load
11local sub, format, find, match = string.sub, string.format, string.find, string.match
12local splitlines, validstring, replacenewlines = string.splitlines, string.valid, string.replacenewlines
13local P, S, C, Ct, Cs, Cp = lpeg.P, lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Cp
14local lpegpatterns, lpegmatch = lpeg.patterns, lpeg.match
15local utfchar  = utf.char
16local nameonly = file.nameonly
17local totable  = string.totable
18local md5hex = md5.hex
19local isfile = lfs.isfile
20local savedata = io.savedata
21
22local trace_run         = false  trackers.register("buffers.run",       function(v) trace_run       = v end)
23local trace_grab        = false  trackers.register("buffers.grab",      function(v) trace_grab      = v end)
24local trace_visualize   = false  trackers.register("buffers.visualize", function(v) trace_visualize = v end)
25
26local report_buffers    = logs.reporter("buffers","usage")
27local report_typeset    = logs.reporter("buffers","typeset")
28local report_grabbing   = logs.reporter("buffers","grabbing")
29
30local context           = context
31local commands          = commands
32
33local implement         = interfaces.implement
34
35local scanners          = tokens.scanners
36local scanstring        = scanners.string
37local scancsname        = scanners.csname
38local scaninteger       = scanners.integer
39local scanboolean       = scanners.boolean
40local scancode          = scanners.code
41local scantokencode     = scanners.tokencode
42
43local getters           = tokens.getters
44local gettoken          = getters.token
45
46local createtoken       = token.create
47local grabtokens        = token.grab
48
49local getcommand        = tokens.accessors.command
50local getnextchar       = tokens.scanners.nextchar
51
52local settings_to_array = utilities.parsers.settings_to_array
53local formatters        = string.formatters
54local addsuffix         = file.addsuffix
55local replacesuffix     = file.replacesuffix
56
57local registertempfile  = luatex.registertempfile
58
59local v_yes             <const> = interfaces.variables.yes
60local v_append          <const> = interfaces.variables.append
61
62local eol               = lpegpatterns.eol
63local space             = lpegpatterns.space
64local whitespace        = lpegpatterns.whitespace
65local blackspace        = whitespace - eol
66local whatever          = (1-eol)^1 * eol^0
67local emptyline         = space^0 * eol
68
69local ctxcatcodes       <const> = catcodes.numbers.ctxcatcodes
70local txtcatcodes       <const> = catcodes.numbers.txtcatcodes
71
72local setdata           = job.datasets.setdata
73local getdata           = job.datasets.getdata
74
75local ctx_viafile          = context.viafile
76local ctx_getbuffer        = context.getbuffer
77local ctx_pushcatcodetable = context.pushcatcodetable
78local ctx_popcatcodetable  = context.popcatcodetable
79local ctx_setcatcodetable  = context.setcatcodetable
80local ctx_printlines       = context.printlines
81
82buffers       = buffers or { }
83local buffers = buffers
84
85local cache = { }
86
87local function erase(name)
88    if not name or name == "" then
89        cache[""] = false -- nil
90    else
91        local list = settings_to_array(name)
92        for i=1,#list do
93            cache[list[i]] = false -- nil
94        end
95    end
96end
97
98local assign  do
99
100    -- There is no gain in an immediate concatenating lpeg
101
102    local action  = whitespace^0 * C(P("+")^1 + P("*")) * whitespace^0
103    local equal   = whitespace^0 * lpegpatterns.equal * whitespace^0
104    local name    = C((1-action)^1)
105    local pattern = C((1-equal)^1) * equal * Ct((action + name)^1)
106
107    assign = function(name,str,catcodes)
108        local target, content = lpegmatch(pattern,name)
109        if target and content then
110            for i=1,#content do
111                local c = content[i]
112                if c == "+" then
113                    content[i] = ""
114                elseif c == "++" then
115                    content[i] = " "
116                elseif c == "+++" then
117                    content[i] = "\r\r"
118                elseif c == "*" then
119                    content[i] = str
120                else
121                    local s = cache[c]
122                    content[i] = s and s.data or ""
123                end
124            end
125            name = target
126            str  = concat(content)
127        end
128        cache[name] = {
129            data     = str,
130            catcodes = catcodes,
131            typeset  = false,
132        }
133    end
134
135end
136
137local prepend, append  do
138
139    local function combine(name,str,prepend)
140        local buffer = cache[name]
141        if buffer then
142            buffer.data    = prepend and (str .. buffer.data) or (buffer.data .. str)
143            buffer.typeset = false
144        else
145            cache[name] = {
146                data     = str,
147                typeset  = false,
148            }
149        end
150    end
151
152    prepend = function(name,str) combine(name,str,true) end
153    append  = function(name,str) combine(name,str)      end
154
155end
156
157local function exists(name)
158    return cache[name]
159end
160
161local function getcontent(name)
162    local buffer = name and cache[name]
163    return buffer and buffer.data or ""
164end
165
166local function empty(name)
167    if find(getcontent(name),"%S") then
168        return false
169    else
170        return true
171    end
172end
173
174local function getlines(name)
175    local buffer = name and cache[name]
176    return buffer and splitlines(buffer.data)
177end
178
179local function getnames(name)
180    if type(name) == "string" then
181        return settings_to_array(name)
182    else
183        return name
184    end
185end
186
187local function istypeset(name)
188    local names = getnames(name)
189    if #names == 0 then
190        return false
191    end
192    for i=1,#names do
193        local c = cache[names[i]]
194        if c and not c.typeset then
195            return false
196        end
197    end
198    return true
199end
200
201local function markastypeset(name)
202    local names  = getnames(name)
203    for i=1,#names do
204        local c = cache[names[i]]
205        if c then
206            c.typeset = true
207        end
208    end
209end
210
211local function collectcontent(name,separator) -- no print
212    local names  = getnames(name)
213    local nnames = #names
214    if nnames == 0 then
215        return getcontent("") -- default buffer
216    elseif nnames == 1 then
217        return getcontent(names[1])
218    else
219        local t = { }
220        local n = 0
221        for i=1,nnames do
222            local c = getcontent(names[i])
223            if c ~= "" then
224                n = n + 1
225                t[n] = c
226            end
227        end
228        -- the default separator was \r, then \n and is now os.newline because buffers
229        -- can be loaded in other applications
230        return concat(t,separator or os.newline)
231    end
232end
233
234local function loadcontent(name) -- no print
235    local content = collectcontent(name,"\n") -- tex likes \n hm, elsewhere \r
236    local ok, err = load(content)
237    if ok then
238        return ok()
239    else
240        report_buffers("invalid lua code in buffer %a: %s",name,err or "unknown error")
241    end
242end
243
244buffers.raw            = getcontent
245buffers.erase          = erase
246buffers.assign         = assign
247buffers.prepend        = prepend
248buffers.append         = append
249buffers.exists         = exists
250buffers.empty          = empty
251buffers.getcontent     = getcontent
252buffers.getlines       = getlines
253buffers.collectcontent = collectcontent
254buffers.loadcontent    = loadcontent
255
256-- the context interface
257
258implement {
259    name      = "assignbuffer",
260    actions   = assign,
261    arguments = { "string", "string", "integer" }
262}
263
264implement {
265    name      = "erasebuffer",
266    actions   = erase,
267    arguments = "string"
268}
269
270-- local anything      = lpegpatterns.anything
271-- local alwaysmatched = lpegpatterns.alwaysmatched
272-- local utf8character = lpegpatterns.utf8character
273--
274-- local function countnesting(b,e)
275--     local n
276--     local g = P(b) / function() n = n + 1 end
277--             + P(e) / function() n = n - 1 end
278--          -- + anything
279--             + utf8character
280--     local p = alwaysmatched / function() n = 0 end
281--             * g^0
282--             * alwaysmatched / function() return n end
283--     return p
284-- end
285
286local counters   = { }
287local nesting    = 0
288local autoundent = true
289local continue   = false
290
291-- Beware: the first character of bufferdata has to be discarded as it's there to
292-- prevent gobbling of newlines in the case of nested buffers. The last one is
293-- a newlinechar and is removed too.
294--
295-- An \n is unlikely to show up as \r is the endlinechar but \n is more generic
296-- for us.
297
298-- This fits the way we fetch verbatim: the indentation before the sentinel
299-- determines the stripping.
300
301-- str = [[
302--     test test test test test test test
303--       test test test test test test test
304--     test test test test test test test
305--
306--     test test test test test test test
307--       test test test test test test test
308--     test test test test test test test
309--     ]]
310
311-- local function undent(str)
312--     local margin = match(str,"[\n\r]( +)[\n\r]*$") or ""
313--     local indent = #margin
314--     if indent > 0 then
315--         local lines = splitlines(str)
316--         local ok = true
317--         local pattern = "^" .. margin
318--         for i=1,#lines do
319--             local l = lines[i]
320--             if find(l,pattern) then
321--                 lines[i] = sub(l,indent+1)
322--             else
323--                 ok = false
324--                 break
325--             end
326--         end
327--         if ok then
328--             return concat(lines,"\n")
329--         end
330--     end
331--     return str
332-- end
333
334-- how about tabs
335
336local strippers  = { }
337local nofspaces  = 0
338
339local normalline = space^0 / function(s) local n = #s if n < nofspaces then nofspaces = n end end
340                 * whatever
341
342local getmargin = (emptyline + normalline)^1
343
344local function undent(str) -- new version, needs testing: todo: not always needed, like in xtables
345    nofspaces = #str
346    local margin = lpegmatch(getmargin,str)
347    if nofspaces == #str or nofspaces == 0 then
348        return str
349    end
350    local stripper = strippers[nofspaces]
351    if not stripper then
352        stripper = Cs(((space^-nofspaces)/"" * whatever + emptyline)^1)
353        strippers[nofspaces] = stripper
354    end
355    return lpegmatch(stripper,str) or str
356end
357
358buffers.undent = undent
359
360local split = table.setmetatableindex(function(t,k)
361    local v = totable(k)
362    t[k] = v
363    return v
364end)
365
366local tochar = {
367    [ 0] = "\\",
368    [ 1] = "{",
369    [ 2] = "}",
370    [ 3] = "$",
371    [ 4] = "&",
372    [ 5] = "\n",
373    [ 6] = "#", -- todo
374    [ 7] = "^",
375    [ 8] = "_",
376    [10] = " ",
377    [14] = "%",
378}
379
380local experiment = false
381local experiment = scantokencode and true
382
383local function pickup(start,stop)
384    local stoplist    = split[stop] -- totable(stop)
385    local stoplength  = #stoplist
386    local stoplast    = stoplist[stoplength]
387    local startlist   = split[start] -- totable(start)
388    local startlength = #startlist
389    local startlast   = startlist[startlength]
390    local list        = { }
391    local size        = 0
392    local depth       = 0
393 -- local done        = 32
394    local scancode    = experiment and scantokencode or scancode
395    while true do -- or use depth
396        local char = scancode()
397        if char then
398          -- if char < done then
399          --     -- we skip leading control characters so that we can use them to
400          --     -- obey spaces (a dirty trick)
401          -- else
402          --     done = 0
403                char = utfchar(char)
404                size = size + 1
405                list[size] = char
406                if char == stoplast and size >= stoplength then
407                    local done = true
408                    local last = size
409                    for i=stoplength,1,-1 do
410                        if stoplist[i] ~= list[last] then
411                            done = false
412                            break
413                        end
414                        last = last - 1
415                    end
416                    if done then
417                        if depth > 0 then
418                            depth = depth - 1
419                        else
420                            break
421                        end
422                        char = false -- trick: let's skip the next (start) test
423                    end
424                end
425                if char == startlast and size >= startlength then
426                    local done = true
427                    local last = size
428                    for i=startlength,1,-1 do
429                        if startlist[i] ~= list[last] then
430                            done = false
431                            break
432                        end
433                        last = last - 1
434                    end
435                    if done then
436                        depth = depth + 1
437                    end
438                end
439         -- end
440        else
441         -- local t = scantoken()
442            local t = gettoken()
443            if t then
444                -- we're skipping leading stuff, like obeyedlines and relaxes
445                if experiment and size > 0 then
446                    -- we're probably in a macro
447                    local char = tochar[getcommand(t)]
448                    if char then
449                        size = size + 1 ; list[size] = char
450                    else
451                     -- local csname = getcsname(t)
452                        local csname = scancsname(t)
453                        if csname == stop then
454                            stoplength = 0
455                            break
456                        else
457                            size = size + 1 ; list[size] = "\\"
458                            size = size + 1 ; list[size] = csname
459                            size = size + 1 ; list[size] = " "
460                        end
461                    end
462                else
463                    -- ignore and hope for the best
464                end
465            else
466                break
467            end
468        end
469    end
470    local start = 1
471    local stop  = size - stoplength - 1
472    -- not good enough: only empty lines, but even then we miss the leading
473    -- for verbatim
474    --
475    -- the next is not yet adapted to the new scanner ... we don't need lpeg here
476    --
477    for i=start,stop do
478        local li = list[i]
479        if lpegmatch(blackspace,li) then
480            -- keep going
481        elseif lpegmatch(eol,li) then
482            -- okay
483            start = i + 1
484        else
485            break
486        end
487    end
488    for i=stop,start,-1 do
489        if lpegmatch(whitespace,list[i]) then
490            stop = i - 1
491        else
492            break
493        end
494    end
495    --
496    if start <= stop then
497        return concat(list,"",start,stop)
498    else
499        return ""
500    end
501end
502
503tokens.pickup = pickup
504
505local function showpickup(name,bufferdata,catcodes,undented)
506    undented = undented and ">" or "="
507    if #bufferdata > 50 then
508        report_grabbing("%s : %i =%s |%s..%s|",name,catcodes,undented,sub(bufferdata,1,20),sub(bufferdata,-20,#bufferdata))
509    else
510        report_grabbing("%s : %i =%s |%s|",name,catcodes,undented,bufferdata)
511    end
512end
513
514implement {
515    name    = "pickupbuffer",
516    actions = function()
517        -- let's pickup all here (no arguments)
518        local name     = scanstring()
519        local start    = scanstring()
520        local stop     = scanstring()
521        local finish   = scancsname()
522        local catcodes = scaninteger()
523        local doundent = scaninteger() == 1 -- better than a keyword scan
524        -- could be a scanner:
525        local data     = pickup(start,stop)
526        local undented = doundent or (autoundent and doundent == nil)
527        if undented then
528            data = undent(data)
529        end
530        if trace_grab then
531            showpickup(name,data,catcodes,undented)
532        end
533        assign(name,data,catcodes)
534        context[finish]()
535    end
536}
537
538implement {
539    name    = "grabbuffer",
540    actions = function()
541        -- let's pickup all here (no arguments)
542        local name     = scanstring()
543        local start    = scanstring()
544        local stop     = scanstring()
545        local finish   = scancsname()
546        local catcodes = scaninteger()
547        local doundent = scaninteger() == 1 -- better than a keyword scan
548        local starttok = createtoken(start,true)
549        local stoptok  = createtoken(stop,true)
550        local data     = grabtokens(starttok,stoptok,true,13) -- strip first and last \endoflineasciicode
551        local undented = doundent or (autoundent and doundent == nil)
552        if undented then
553            data = undent(data)
554        end
555        if trace_grab then
556            showpickup(name,data,catcodes,undented)
557        end
558        assign(name,data,catcodes)
559        context[finish]()
560    end
561}
562
563local function savebuffer(list,name,prefix,option,directory) -- name is optional
564    if not list or list == "" then
565        list = name
566    end
567    if not name or name == "" then
568        name = list
569    end
570    local content = collectcontent(list,nil) or ""
571    if content == "" then
572        content = "empty buffer"
573    end
574    if prefix == v_yes then
575        name = addsuffix(tex.jobname .. "-" .. name,"tmp")
576    end
577    if directory ~= "" and dir.makedirs(directory) then
578        name = file.join(directory,name)
579    end
580    savedata(name,replacenewlines(content),"\n",option == v_append)
581end
582
583implement {
584    name      = "savebuffer",
585    actions   = savebuffer,
586    arguments = "5 strings",
587}
588
589-- we can consider adding a size to avoid unlikely clashes
590
591local olddata   = nil
592local newdata   = nil
593local getrunner = sandbox.getrunner
594
595local runner = sandbox.registerrunner {
596    name     = "run buffer",
597    program  = "context",
598    method   = "execute",
599    template = [[--purgeall %?path: --path=%path% ?% %filename%]],
600    reporter = report_typeset,
601    checkers = {
602        filename = "readable",
603        path     = "string",
604    }
605}
606
607local function runbuffer(name,encapsulate,runnername,suffixes)
608    if not runnername or runnername == "" then
609        runnername = "run buffer"
610    end
611    local suffix = "pdf"
612    if type(suffixes) == "table" then
613        suffix = suffixes[1]
614    elseif type(suffixes) == "string" and suffixes ~= "" then
615        suffix   = suffixes
616        suffixes = { suffix }
617    else
618        suffixes = { suffix }
619    end
620    local runner = getrunner(runnername)
621    if not runner then
622        report_typeset("unknown runner %a",runnername)
623        return
624    end
625    if not olddata then
626        olddata = getdata("buffers","runners") or { }
627        local suffixes = olddata.suffixes
628        local hashes   = olddata.hashes
629        if hashes and suffixes then
630            for k, hash in next, hashes do
631                for h, v in next, hash do
632                    for s, v in next, suffixes do
633                        local tmp = addsuffix(h,s)
634                     -- report_typeset("mark for deletion: %s",tmp)
635                        registertempfile(tmp)
636                    end
637                end
638            end
639        end
640    end
641    if not newdata then
642        newdata = {
643            version  = environment.version,
644            suffixes = { },
645            hashes   = { },
646        }
647        setdata {
648            name = "buffers",
649            tag  = "runners",
650            data = newdata,
651        }
652    end
653    local oldhashes = olddata.hashes or { }
654    local newhashes = newdata.hashes or { }
655    local old = oldhashes[suffix]
656    local new = newhashes[suffix]
657    if not old then
658        old = { }
659        oldhashes[suffix] = old
660        for hash, n in next, old do
661            local tag = formatters["%s-t-b-%s"](tex.jobname,hash)
662            local tmp = addsuffix(tag,"tmp")
663         -- report_typeset("mark for deletion: %s",tmp)
664            registertempfile(tmp) -- to be sure
665        end
666    end
667    if not new then
668        new = { }
669        newhashes[suffix] = new
670    end
671    local names   = getnames(name)
672    local content = collectcontent(names,nil) or ""
673    if content == "" then
674        content = "empty buffer"
675    end
676    if encapsulate then
677        if not find(content,"\\starttext") then
678            content = formatters["\\starttext\n%s\n\\stoptext\n"](content)
679        end
680    end
681    --
682    local hash = md5hex(content)
683    local tag  = formatters["%s-t-b-%s"](nameonly(tex.jobname),hash) -- make sure we run on the local path
684    --
685    local filename   = addsuffix(tag,"tmp")
686    local resultname = addsuffix(tag,suffix)
687    --
688    if new[tag] then
689        -- done
690    elseif not old[tag] or olddata.version ~= newdata.version or not isfile(resultname) then
691        if trace_run then
692            report_typeset("changes in %a, processing forced",name)
693        end
694        savedata(filename,content)
695        report_typeset("processing saved buffer %a\n",filename)
696        runner {
697            filename = filename,
698            path     = environment.arguments.path, -- maybe take all set paths
699        }
700    end
701    new[tag] = (new[tag] or 0) + 1
702    report_typeset("no changes in %a, processing skipped",name)
703    registertempfile(filename)
704 -- report_typeset("mark for persistence: %s",filename)
705    for i=1,#suffixes do
706        local suffix = suffixes[i]
707        newdata.suffixes[suffix] = true
708        local tmp = addsuffix(tag,suffix)
709     -- report_typeset("mark for persistance: %s",tmp)
710        registertempfile(tmp,nil,true)
711    end
712    --
713    return resultname -- first result
714end
715
716local f_getbuffer = formatters["buffer.%s"]
717local defaultlist = { "" }
718
719local function getbuffer(name)
720    local list = name and name ~= "" and settings_to_array(name) or defaultlist
721    for i=1,#list do
722        local buf = list[i]
723        local str = getcontent(buf)
724        if str ~= "" then
725         -- characters.showstring(str)
726            ctx_viafile(str,f_getbuffer(validstring(buf,"noname")))
727        end
728    end
729end
730
731local function getbuffermkvi(name) -- rather direct !
732    ctx_viafile(resolvers.macros.preprocessed(getcontent(name)),formatters["buffer.%s.mkiv"](validstring(name,"noname")))
733end
734
735local function getbuffertex(name)
736    local buffer = name and cache[name]
737    if buffer and buffer.data ~= "" then
738        ctx_pushcatcodetable()
739        if buffer.catcodes == txtcatcodes then
740            ctx_setcatcodetable(txtcatcodes)
741        else
742            ctx_setcatcodetable(ctxcatcodes)
743        end
744     -- context(function() ctx_viafile(buffer.data) end)
745        ctx_getbuffer { name } -- viafile flushes too soon
746        ctx_popcatcodetable()
747    end
748end
749
750buffers.get       = getbuffer
751buffers.getmkvi   = getbuffermkvi
752buffers.gettex    = getbuffertex
753buffers.getctxlua = loadcontent
754buffers.run       = runbuffer
755
756implement { name = "getbufferctxlua", actions = loadcontent,   arguments = "argument" }
757implement { name = "getbuffer",       actions = getbuffer,     arguments = "argument" }
758implement { name = "getbuffermkvi",   actions = getbuffermkvi, arguments = "argument" }
759implement { name = "getbuffertex",    actions = getbuffertex,  arguments = "argument" }
760
761implement {
762    name      = "getbuffercontent",
763    arguments = "string",
764    actions   = { getcontent, context },
765}
766
767implement {
768    name      = "typesetbuffer",
769    actions   = { runbuffer, context },
770    arguments = { "string", true }
771}
772
773implement {
774    name      = "runbuffer",
775    actions   = { runbuffer, context },
776    arguments = { "string", false, "string" }
777}
778
779implement {
780    name      = "doifelsebuffer",
781    actions   = { exists, commands.doifelse },
782    public    = true,
783 -- protected = false,
784    arguments = "string"
785}
786
787implement {
788    name      = "doifelsebufferempty",
789    actions   = { empty, commands.doifelse },
790    public    = true,
791    protected = true,
792    arguments = "string"
793}
794
795-- This only used for mp buffers and is a kludge. Don't change the
796-- texprint into texsprint as it fails because "p<nl>enddef" becomes
797-- "penddef" then.
798
799implement {
800    name      = "feedback", -- bad name, maybe rename to injectbuffercontent
801    actions   = { collectcontent, ctx_printlines },
802    arguments = "string"
803}
804
805do
806
807    local context             = context
808    local ctxcore             = context.core
809
810    local ctx_startbuffer     = ctxcore.startbuffer
811    local ctx_stopbuffer      = ctxcore.stopbuffer
812
813    local ctx_startcollecting = context.startcollecting
814    local ctx_stopcollecting  = context.stopcollecting
815
816    function ctxcore.startbuffer(...)
817        ctx_startcollecting()
818        ctx_startbuffer(...)
819    end
820
821    function ctxcore.stopbuffer()
822        ctx_stopbuffer()
823        ctx_stopcollecting()
824    end
825
826end
827
828-- moved here:
829
830local findfile    = resolvers.findfile
831local setfilename = luatex.synctex.setfilename
832
833function buffers.samplefile(name,before,after,inbetween)
834    local fullname = findfile(name)
835    if fullname and fullname ~= "" then
836        if not exists(name) then
837            assign(name,io.loaddata(fullname))
838        end
839        setfilename(fullname)
840        getbuffer(name)
841    end
842end
843
844implement {
845    name      = "samplefile", -- bad name, maybe rename to injectbuffercontent
846    public    = true,
847    protected = true,
848    actions   = buffers.samplefile,
849    arguments = "string"
850}
851
852function buffers.splitsamplefile(name,before,after,inbetween)
853    local fullname = findfile(name)
854    if fullname and fullname ~= "" then
855        local data  = io.loaddata(fullname) or ""
856        local split = string.split(utilities.strings.striplines(data,"prune and collapse"),lpegpatterns.whitespace^1)
857        local size  = #split
858        local before = tonumber(before) or 1
859        local after  = tonumber(after) or 1
860        if before + after < size then
861            table.move(split,size-after,size,before+1)
862            if inbetween and inbetween ~= "" then
863                split[before+1] = inbetween
864            end
865            data = concat(split, " ",1,before+after+1)
866        end
867        assign(name,data)
868        setfilename(fullname)
869        getbuffer(name)
870    end
871end
872
873implement {
874    name      = "splitsamplefile", -- bad name, maybe rename to injectbuffercontent
875    public    = true,
876    protected = true,
877    actions   = buffers.splitsamplefile,
878    arguments = "4 strings",
879}
880
881-- A somewhat strange place (for now) so the *.log definitions might move someplace
882-- else (if useful at all).
883
884-- Handy for the math test suite that Mikael Sundqvist and I are making where we
885-- need to track box content as well as some low level math tracing features, so
886-- we can pipe to buffers (via a temporary file).
887
888do
889
890    local insert, remove = table.insert, table.remove
891    local setlogfile = texio.setlogfile
892    local openfile   = io.open
893
894    local stack = { }
895    local files = { }
896
897    local function resetlogfile(name)
898        files[name] = false
899    end
900
901    local function pushlogfile(name)
902        local f = openfile(name,files[name] and "ab" or "wb")
903        insert(stack,f)
904        files[name] = true
905        setlogfile(f)
906    end
907
908    local function poplogfile()
909        remove(stack)
910        setlogfile(stack[#stack])
911    end
912
913    logs.pushlogfile  = pushlogfile
914    logs.poplogfile   = poplogfile
915    logs.resetlogfile = resetlogfile
916
917    implement {
918        name      = "resetlogfile",
919        arguments = "argument",
920        public    = true,
921        protected = true,
922        actions   = resetlogfile,
923    }
924
925    implement {
926        name      = "pushlogfile",
927        arguments = "argument",
928        public    = true,
929        protected = true,
930        actions   = pushlogfile,
931    }
932
933    implement {
934        name      = "poplogfile",
935        public    = true,
936        protected = true,
937        actions   = poplogfile,
938    }
939
940    -- In the end we went for a somewhat hidden low level one (see low level math tests
941    -- for usage):
942
943    local serialized = nodes.nuts.serialized
944    local getbox     = nodes.nuts.getbox
945
946    implement {
947        name      = "showboxinbuffer",
948        public    = true,
949        protected = true,
950        arguments = { "argument", "integer", "integer" },
951        actions   = function(buffer, box, detail)
952            local box = getbox(box)
953            assign(buffer or "",box and serialized(box,detail))
954        end,
955    }
956
957end
958
959do
960
961    local integer_value <const> = tokens.values.integer
962
963    local p_newline = lpegpatterns.newline
964
965    local amount
966
967    local p_count = (
968        p_newline / function() amount = amount + 1 end
969      + 1
970    )^0
971
972    local p_width = (
973        (
974            Cp() * (lpegpatterns.utf8char-p_newline)^1 * Cp() * (p_newline + P(0))
975        ) / function(f,t) local l = t - f ; if l > amount then amount = l end end
976      + p_newline
977    )^0
978
979    implement {
980        name      = "nofbufferlines",
981        public    = true,
982        usage     = "value",
983        arguments = "string",
984        actions   = function(n)
985            amount = 0
986            local data = getcontent(n)
987            if data and data ~= "" then
988                lpegmatch(p_count,data)
989            end
990            return integer_value, amount
991        end
992    }
993
994    implement {
995        name      = "maxbufferwidth",
996        public    = true,
997        usage     = "value",
998        arguments = "string",
999        actions   = function(n)
1000            amount = 0
1001            local data = getcontent(n)
1002            if data and data ~= "" then
1003                lpegmatch(p_width,data)
1004            end
1005            return integer_value, amount
1006        end
1007    }
1008
1009end
1010