core-ctx.lua /size: 13 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['core-ctx'] = {
2    version   = 1.001,
3    comment   = "companion to core-ctx.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--[[
10Job control files aka ctx files are rather old and date from the mkii times.
11They were handled in texexec and mtx-context and deals with modes, modules,
12environments and preprocessing in projects where one such file drives the
13processing of lots of files without the need to provide command line
14arguments.
15
16In mkiv this concept was of course supported as well. The first implementation
17of mtx-context took much of the approach of texexec, but by now we have gotten
18rid of the option file (for passing modes, modules and environments), the stubs
19(for directly processing cld and xml) as well as the preprocessing component
20of the ctx files. Special helper features, like typesetting listings, were
21already moved to the extras (a direct side effect of the ability to pass along
22command line arguments.) All this made mtx-context more simple than its ancestor
23texexec.
24
25Because some of the modes might affect the mtx-context end, the ctx file is
26still loaded there but only for getting the modes. The file is loaded again
27during the run but as loading and basic processing takes less than a
28millisecond it's not that much of a burden.
29--]]
30
31-- the ctxrunner tabel might either become private or move to the job namespace
32-- which also affects the loading order
33
34local trace_prepfiles = false  trackers.register("system.prepfiles", function(v) trace_prepfiles = v end)
35
36local tostring = tostring
37local gsub, find, match, validstring = string.gsub, string.find, string.match, string.valid
38local concat = table.concat
39local xmltext = xml.text
40
41local report_jobfile   = logs.reporter("system","jobfile")
42local report_prepfiles = logs.reporter("system","prepfiles")
43
44local commands         = commands
45local implement        = interfaces.implement
46
47ctxrunner              = ctxrunner or { }
48
49ctxrunner.prepfiles    = utilities.storage.allocate()
50
51local function dontpreparefile(t,k)
52    return k -- we only store when we have a prepper
53end
54
55table.setmetatableindex(ctxrunner.prepfiles,dontpreparefile)
56
57local function filtered(str,method) -- in resolvers?
58    str = tostring(str)
59    if     method == 'name'     then str = file.nameonly(str)
60    elseif method == 'path'     then str = file.dirname(str)
61    elseif method == 'suffix'   then str = file.suffix(str)
62    elseif method == 'nosuffix' then str = file.removesuffix(str)
63    elseif method == 'nopath'   then str = file.basename(str)
64    elseif method == 'base'     then str = file.basename(str)
65--  elseif method == 'full'     then
66--  elseif method == 'complete' then
67--  elseif method == 'expand'   then -- str = file.expandpath(str)
68    end
69    return (gsub(str,"\\","/"))
70end
71
72-- local function substitute(e,str)
73--     local attributes = e.at
74--     if str and attributes then
75--         if attributes['method'] then
76--             str = filtered(str,attributes['method'])
77--         end
78--         if str == "" and attributes['default'] then
79--             str = attributes['default']
80--         end
81--     end
82--     return str
83-- end
84
85local function substitute(str)
86    return str
87end
88
89local function justtext(str)
90    str = xml.unescaped(tostring(str))
91    str = xml.cleansed(str)
92    str = gsub(str,"\\+",'/')
93    str = gsub(str,"%s+",' ')
94    return str
95end
96
97function ctxrunner.load(ctxname)
98
99    report_jobfile("processing %a",ctxname)
100
101    local xmldata = xml.load(ctxname)
102
103    local jobname = tex.jobname -- todo
104
105    local variables   = { job = jobname }
106    local commands    = { }
107    local flags       = { }
108    local paths       = { } -- todo
109    local treatments  = { }
110    local suffix      = "prep"
111
112    xml.include(xmldata,'ctx:include','name', {'.', file.dirname(ctxname), "..", "../.." })
113
114    for e in xml.collected(xmldata,"/ctx:job/ctx:flags/ctx:flag") do
115        local flag = xmltext(e)
116        local key, value = match(flag,"^(.-)=(.+)$")
117        if key and value then
118            environment.setargument(key,value)
119        else
120            environment.setargument(flag,true)
121        end
122    end
123
124    -- add to document.options.ctxfile[...]
125
126    local ctxfile  = document.options.ctxfile
127
128    local modes        = ctxfile.modes
129    local modules      = ctxfile.modules
130    local environments = ctxfile.environments
131
132    for e in xml.collected(xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:mode") do
133        modes[#modes+1] = xmltext(e)
134    end
135
136    for e in xml.collected(xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:module") do
137        modules[#modules+1] = xmltext(e)
138    end
139
140    for e in xml.collected(xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:environment") do
141        environments[#environments+1] = xmltext(e)
142    end
143
144    for e in xml.collected(xmldata,"ctx:message") do
145        report_jobfile("ctx comment: %s", xmltext(e))
146    end
147
148    for r, d, k in xml.elements(xmldata,"ctx:value[@name='job']") do
149        d[k] = variables['job'] or ""
150    end
151
152    for e in xml.collected(xmldata,"/ctx:job/ctx:preprocess/ctx:processors/ctx:processor") do
153        local name   = e.at and e.at['name'] or "unknown"
154        local suffix = e.at and e.at['suffix'] or "prep"
155        for r, d, k in xml.elements(command,"ctx:old") do
156            d[k] = "%old%"
157        end
158        for r, d, k in xml.elements(e,"ctx:new") do
159            d[k] = "%new%"
160        end
161        for r, d, k in xml.elements(e,"ctx:value") do
162            local tag = d[k].at['name']
163            if tag then
164                d[k] = "%" .. tag .. "%"
165            end
166        end
167        local runner = xml.textonly(e)
168        if runner and runner ~= "" then
169            commands[name] = {
170                suffix = suffix,
171                runner = runner,
172            }
173        end
174    end
175
176    local suffix   = xml.filter(xmldata,"xml:///ctx:job/ctx:preprocess/attribute('suffix')") or suffix
177    local runlocal = xml.filter(xmldata,"xml:///ctx:job/ctx:preprocess/ctx:processors/attribute('local')")
178
179    runlocal = toboolean(runlocal)
180
181    -- todo: only collect, then plug into file handler
182
183    local inputfile = validstring(environment.arguments.input) or jobname
184
185    variables.old = inputfile
186
187    for files in xml.collected(xmldata,"/ctx:job/ctx:preprocess/ctx:files") do
188        for pattern in xml.collected(files,"ctx:file") do
189            local preprocessor = pattern.at['processor'] or ""
190            for r, d, k in xml.elements(pattern,"/ctx:old") do
191                d[k] = jobname
192            end
193            for r, d, k in xml.elements(pattern,"/ctx:value[@name='old'") do
194                d[k] = jobname
195            end
196            pattern =justtext(xml.tostring(pattern))
197            if preprocessor and preprocessor ~= "" and pattern and pattern ~= "" then
198                local noftreatments = #treatments + 1
199                local findpattern   = string.topattern(pattern)
200                local preprocessors = utilities.parsers.settings_to_array(preprocessor)
201                treatments[noftreatments] = {
202                    pattern       = findpattern,
203                    preprocessors = preprocessors,
204                }
205                report_jobfile("step %s, pattern %a, preprocessor: %a",noftreatments,findpattern,preprocessors)
206             end
207        end
208    end
209
210    if #treatments == 0 then
211        report_jobfile("no treatments needed")
212    end
213
214    local function needstreatment(oldfile)
215        for i=1,#treatments do
216            local treatment = treatments[i]
217            local pattern = treatment.pattern
218            if find(oldfile,pattern) then
219                return treatment
220            end
221        end
222    end
223
224    local preparefile = #treatments > 0 and function(prepfiles,filename)
225
226        filename = file.collapsepath(filename)
227
228        local treatment = needstreatment(filename)
229        local oldfile = filename
230        local newfile = false
231        if treatment then
232            local preprocessors = treatment.preprocessors
233            local runners = { }
234            for i=1,#preprocessors do
235                local preprocessor = preprocessors[i]
236                local command = commands[preprocessor]
237                if command then
238                    local runner = command.runner
239                    local suffix = command.suffix
240                    local result = filename .. "." .. suffix
241                    if runlocal then
242                        result = file.basename(result)
243                    end
244                    variables.old = oldfile
245                    variables.new = result
246                    runner = utilities.templates.replace(runner,variables)
247                    if runner and runner ~= "" then
248                        runners[#runners+1] = runner
249                        oldfile = result
250                        if runlocal then
251                            oldfile = file.basename(oldfile)
252                        end
253                        newfile = oldfile
254                    end
255                end
256            end
257            oldname = file.collapsepath(oldname)
258            newname = file.collapsepath(newname)
259            if not newfile then
260                newfile = filename
261                report_prepfiles("%a is not converted to %a",filename,newfile)
262            elseif not lfs.isfile(newfile) or file.needsupdating(filename,newfile) then
263                for i=1,#runners do
264                    report_prepfiles("step %i: %s",i,runners[i])
265                end
266                --
267                for i=1,#runners do
268                    local command = runners[i]
269                    report_prepfiles("command: %s",command)
270                    --
271                    -- remark: we don't use sandbox.registerrunner here as we cannot predict what
272                    -- gets done here, so just:
273                    --
274                    local result = os.execute(command) or 0
275                    --
276                 -- if result > 0 then
277                 --     report_prepfiles("error, return code: %s",result)
278                 -- end
279                    logs.newline()
280                    logs.newline()
281                end
282                if lfs.isfile(newfile) then
283                    file.syncmtimes(filename,newfile)
284                    report_prepfiles("%a is converted to %a",filename,newfile)
285                else
286                    report_prepfiles("%a is not converted to %a",filename,newfile)
287                    newfile = filename
288                end
289            elseif lfs.isfile(newfile) then
290                report_prepfiles("%a is already converted to %a",filename,newfile)
291            else
292                report_prepfiles("unknown error when converting %a to %a",filename,newfile)
293            end
294        else
295            newfile = filename
296        end
297        prepfiles[filename] = newfile
298        -- in case we ask twice (with the prepped name) ... todo: avoid this mess
299        prepfiles[newfile]  = newfile
300        return newfile
301    end
302
303    table.setmetatableindex(ctxrunner.prepfiles,preparefile or dontpreparefile)
304
305    -- we need to deal with the input filename as it has already be resolved
306
307end
308
309--     print("\n")
310--     document = {
311--         options =  {
312--             ctxfile = {
313--                 modes        = { },
314--                 modules      = { },
315--                 environments = { },
316--             }
317--         }
318--     }
319--     environment.arguments.input = "test.tex"
320--     ctxrunner.load("x-ldx.ctx")
321
322local function resolve(name) -- used a few times later on
323    return ctxrunner.prepfiles[file.collapsepath(name)] or false
324end
325
326function ctxrunner.preparedfile(name)
327    return resolve(name) or name
328end
329
330local ctx_processfile       = commands.processfile
331local ctx_doifelseinputfile = commands.doifelseinputfile
332
333implement {
334    name      = "processfile",
335    overload  = true,
336    arguments = { "string", "integer" },
337    actions   = function(name,maxreadlevel) -- overloaded
338        local prepname = resolve(name)
339        if prepname then
340            return ctx_processfile(prepname,0)
341        else
342            return ctx_processfile(name,maxreadlevel)
343        end
344    end
345}
346
347implement {
348    name      = "doifelseinputfile",
349    overload  = true,
350    arguments = { "string", "integer" },
351    actions   = function(name,depth)
352        local prepname = resolve(name)
353        if prepname then
354            return ctx_doifelseinputfile(prepname,0)
355        else
356            return ctx_doifelseinputfile(name,depth)
357        end
358    end
359}
360
361-- implement {
362--     name      = "preparedfile", -- not used
363--     arguments = "string",
364--     actions   = { ctxrunner.preparedfile, context }
365-- }
366
367implement {
368    name      = "setdocumentctxfile",
369    onlyonce  = true,
370    actions   = function()
371        local ctxfile = document.arguments.ctx or ""
372        if ctxfile ~= "" then
373            ctxrunner.load(ctxfile) -- do we need to locate it?
374        end
375    end
376}
377
378function ctxrunner.resolve(name) -- used a few times later on
379    local collapsedname = file.collapsepath(name,".")
380    return ctxrunner.prepfiles[collapsedname] or collapsedname
381end
382
383-- ctxrunner.load("t:/sources/core-ctx.ctx")
384
385-- context(ctxrunner.prepfiles["one-a.xml"]) context.par()
386-- context(ctxrunner.prepfiles["one-b.xml"]) context.par()
387-- context(ctxrunner.prepfiles["two-c.xml"]) context.par()
388-- context(ctxrunner.prepfiles["two-d.xml"]) context.par()
389-- context(ctxrunner.prepfiles["all-x.xml"]) context.par()
390
391-- inspect(ctxrunner.prepfiles)
392