syst-lua.lmt /size: 16 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['syst-lua'] = {
2    version   = 1.001,
3    comment   = "companion to syst-lua.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 load, type, tonumber = load, type, tonumber
10local find, gsub = string.find, string.gsub
11local concat = table.concat
12local utfchar = utf.char
13local S, C, P, lpegmatch, lpegtsplitat = lpeg.S, lpeg.C, lpeg.P, lpeg.match, lpeg.tsplitat
14
15local xmath                    = xmath    or math
16local xcomplex                 = xcomplex or { }
17
18----- scannext                 = token.scannext
19local scancmdchr               = token.scancmdchrexpanded
20local scantoken                = token.scantoken
21local getcsname                = token.getcsname
22
23local cmd                      = tokens.commands
24local letter_code              = cmd.letter
25local other_char_code          = cmd.other_char
26local spacer_code              = cmd.spacer
27local other_char_code          = cmd.other_char
28local relax_code               = cmd.relax
29local register_int_code        = cmd.register_int
30local internal_int_code        = cmd.internal_int
31local register_dimen_code      = cmd.register_dimen
32local internal_dimen_code      = cmd.internal_dimen
33local register_glue_code       = cmd.register_glue
34local internal_glue_code       = cmd.internal_glue
35local register_toks_code       = cmd.register_toks
36local internal_toks_code       = cmd.internal_toks
37local char_given_code          = cmd.char_given
38local math_given_code          = cmd.math_given
39local xmath_given_code         = cmd.xmath_given
40local some_item_code           = cmd.some_item
41
42local getdimen                 = tex.getdimen
43local getglue                  = tex.getglue
44local getcount                 = tex.getcount
45local gettoks                  = tex.gettoks
46local gettex                   = tex.get
47
48local context                  = context
49commands                       = commands or { }
50local commands                 = commands
51local context                  = context
52local implement                = interfaces.implement
53local dimenfactors             = number.dimenfactors
54
55local ctx_protected_cs         = context.protected.cs -- more efficient
56local ctx_firstoftwoarguments  = context.firstoftwoarguments
57local ctx_secondoftwoarguments = context.secondoftwoarguments
58local ctx_firstofoneargument   = context.firstofoneargument
59local ctx_gobbleoneargument    = context.gobbleoneargument
60
61local boolean_value <const> = tokens.values.boolean
62
63implement { -- will be overloaded later
64    name      = "writestatus",
65    arguments = "2 arguments",
66    actions   = logs.status,
67}
68
69function commands.doifelse(b)
70    if b then
71        ctx_firstoftwoarguments()
72    else
73        ctx_secondoftwoarguments()
74    end
75end
76
77function commands.doifelsesomething(b)
78    if b and b ~= "" then
79        ctx_firstoftwoarguments()
80    else
81        ctx_secondoftwoarguments()
82    end
83end
84
85function commands.doif(b)
86    if b then
87        ctx_firstofoneargument()
88    else
89        ctx_gobbleoneargument()
90    end
91end
92
93function commands.doifsomething(b)
94    if b and b ~= "" then
95        ctx_firstofoneargument()
96    else
97        ctx_gobbleoneargument()
98    end
99end
100
101function commands.doifnot(b)
102    if b then
103        ctx_gobbleoneargument()
104    else
105        ctx_firstofoneargument()
106    end
107end
108
109function commands.doifnotthing(b)
110    if b and b ~= "" then
111        ctx_gobbleoneargument()
112    else
113        ctx_firstofoneargument()
114    end
115end
116
117commands.testcase = commands.doifelse -- obsolete
118
119function commands.boolcase(b)
120    context(b and 1 or 0)
121end
122
123function commands.doifelsespaces(str)
124    if find(str,"^ +$") then
125        ctx_firstoftwoarguments()
126    else
127        ctx_secondoftwoarguments()
128    end
129end
130
131local pattern = lpeg.patterns.validdimen
132
133function commands.doifelsedimenstring(str)
134    if lpegmatch(pattern,str) then
135        ctx_firstoftwoarguments()
136    else
137        ctx_secondoftwoarguments()
138    end
139end
140
141local p_first = C((1-P(",")-P(-1))^0)
142
143implement {
144    name      = "firstinset",
145    arguments = "string",
146    actions   = function(str) context(lpegmatch(p_first,str or "")) end,
147    public    = true,
148}
149
150implement {
151    name      = "ntimes",
152    arguments = { "string", "integer" },
153    actions   = { string.rep, context }
154}
155
156implement {
157    name      = "execute",
158    arguments = "string",
159    actions   = os.execute -- wrapped in sandbox
160}
161
162implement {
163    name      = "doifelsesame",
164    arguments = "2 strings",
165    actions   = function(a,b)
166        if a == b then
167            ctx_firstoftwoarguments()
168        else
169            ctx_secondoftwoarguments()
170        end
171    end
172}
173
174implement {
175    name      = "doifsame",
176    arguments = "2 strings",
177    actions   = function(a,b)
178        if a == b then
179            ctx_firstofoneargument()
180        else
181            ctx_gobbleoneargument()
182        end
183    end
184}
185
186implement {
187    name      = "doifnotsame",
188    arguments = "2 strings",
189    actions   = function(a,b)
190        if a == b then
191            ctx_gobbleoneargument()
192        else
193            ctx_firstofoneargument()
194        end
195    end
196}
197
198-- This is a bit of a joke as I never really needed floating point expressions (okay,
199-- maybe only with scaling because there one can get numbers that are too large for
200-- dimensions to deal with). Of course one can write a parser in \TEX\ speak but then
201-- one also needs to implement a bunch of functions. It doesn't pay of so we just
202-- stick to the next gimmick. It looks inefficient but performance is actually quite
203-- efficient.
204
205do
206
207    local result = { "return " }
208    local word   = { }
209    local r      = 1
210    local w      = 0
211
212    local report = logs.reporter("system","expression")
213
214    local function unexpected(c)
215        report("unexpected token %a",c)
216    end
217
218    local function unexpected(c)
219        report("unexpected token %a",c)
220    end
221
222    local function expression()
223        local w = 0
224        local r = 1
225        while true do
226        local n, i = scancmdchr()
227            if n == letter_code then
228                w = w + 1 ; word[w] = utfchar(i)
229            else
230                if w > 0 then
231                    -- we could use a metatable for all math, complex and factors
232                    local s = concat(word,"",1,w)
233                    local d = dimenfactors[s]
234                    if d then
235                        r = r + 1 ; result[r] = "*"
236                        r = r + 1 ; result[r] = 1/d
237                    else
238                        if xmath[s] then
239                            r = r + 1 ; result[r] = "xmath."
240                        elseif xcomplex[s] then
241                            r = r + 1 ; result[r] = "xcomplex."
242                        end
243                        r = r + 1 ; result[r] = s
244                    end
245                    w = 0
246                end
247                if     n == other_char_code then
248                    r = r + 1 ; result[r] = utfchar(i)
249                elseif n == spacer_code then
250                -- r = r + 1 ; result[r] = " "
251                elseif n == relax_code then
252                    break
253                elseif n == register_int_code or n == internal_int_code then
254                    r = r + 1 ; result[r] = getcount(i)
255                elseif n == register_dimen_code or n == internal_dimen_code then
256                    r = r + 1 ; result[r] = getdimen(i)
257                elseif n == register_glue_code or n == n == register_dimen_code_glue_code then
258                    r = r + 1 ; result[r] = getglue(i)
259                elseif n == register_toks_code or n == n == register_dimen_code_toks_code then
260                    r = r + 1 ; result[r] = gettoks(i)
261                elseif n == char_given_code or n == math_given_code or n == xmath_given_code then
262                    r = r + 1 ; result[r] = i
263                elseif n == some_item_code then
264                    local n = getcsname(t)
265                    if n then
266                        local s = gettex(n)
267                        if s then
268                            r = r + 1 ; result[r] = s
269                        else
270                            unexpected(c)
271                        end
272                    else
273                        unexpected(c)
274                    end
275             -- elseif n == call_code then
276             --     local n = getcsname(t)
277             --     if n then
278             --         local s = get_macro(n)
279             --         if s then
280             --             r = r + 1 ; result[r] = s
281             --         else
282             --             unexpected(c)
283             --         end
284             --     else
285             --         unexpected(c)
286             --     end
287             -- elseif n == the_code or n == convert_code or n == lua_expandable_call_code then
288             --     put_next(t)
289             --     scantoken() -- expands
290                else
291                    unexpected(c)
292                end
293            end
294        end
295        local code = concat(result,"",1,r)
296        local func = load(code)
297        if type(func) == "function" then
298            context(func())
299        else
300            report("invalid lua %a",code)
301        end
302    end
303
304    implement {
305        public  = true,
306        name    = "expression",
307        actions = expression,
308    }
309
310end
311
312do
313
314    interfaces.implement {
315        name      = "iflua",
316        public    = true,
317        usage     = "condition",
318        arguments = "string",
319        actions   = function(s)
320            local c = load("return(" .. s .. ")")
321            return boolean_code, (c and c()) and true or false
322        end,
323    }
324
325end
326
327do
328
329    -- This is some 20% slower than native but we only provide this for compatibility
330    -- reasons so we don't care that much about it. Eventually we can drop the built-in
331    -- method.
332
333    local channels = { }
334    local lastdone = { }
335
336    local findbinfile = resolvers.findbinfile
337    local loadbinfile = resolvers.loadbinfile
338    local opentexfile = resolvers.opentexfile
339
340    local scaninteger = tokens.scanners.integer
341    local scankeyword = tokens.scanners.keyword
342    local scanstring  = tokens.scanners.string
343    local scancsname  = tokens.scanners.csname
344
345    local setmacro    = tokens.setters.macro
346    local rlncatcodes = tex.rlncatcodes
347    local texgetcount = tex.getcount
348
349    local bytes       = string.bytes
350    local getcatcode  = tex.getcatcode
351
352    local char, concat = string.char, table.concat
353
354    -- This uses the normal bin lookup method that we also use for other files,
355    -- and in principle there is no limit on the amount of files other than the
356    -- operating system imposes.
357
358    local t = { }
359    local l = 0
360
361    implement {
362        name    = "openin",
363        public  = true,
364        usage   = "value",
365        actions = function()
366            local n = scaninteger()
367                      scankeyword("=")
368            local s = scanstring(true)
369            local c = channels[n]
370            if c then
371                c:close()
372            end
373            local f = findbinfile(s,"tex")
374            if f then
375                channels[n] = opentexfile(f)
376                lastdone[n] = false
377            else
378                channels[n] = false
379                lastdone[n] = true
380            end
381        end,
382    }
383
384    implement {
385        name    = "closein",
386        public  = true,
387        usage   = "value",
388        actions = function()
389            local n = scaninteger()
390            local c = channels[n]
391            if c then
392                c:close()
393            end
394            channels[n] = false
395            t = { }
396        end,
397    }
398
399    -- This is not the fastest routine but hardly used, occasionally for line by line
400    -- input in e.g. tikz which is not that fast anyway so I'll deal with it when it
401    -- really is a bottleneck.
402
403    local readerror = function(what,n)
404        tex.error(string.formatters["too many %s brace tokens in read from channel %i"](what,n))
405    end
406
407    interfaces.implement {
408        name    = "read",
409        public  = true,
410        usage   = "value",
411        actions = function(prefix)
412            local n = scaninteger()
413            local c = channels[n]
414            if scankeyword("line") and c then
415                c:gotoline(scaninteger())
416            end
417            scankeyword("to")
418            local m = scancsname(true)
419            local g = 0
420            local s
421            l = 0
422            if c then
423                while true do
424                    local s = c and c:reader()
425                    if s then
426                        l = l + 1
427                        s = gsub(s," *$","")
428                        t[l] = s
429                        for c in bytes(s) do
430                            local cc = getcatcode(c)
431                            if cc == 1 then
432                                g = g + 1
433                            elseif cc == 2 then
434                                g = g - 1
435                            end
436                        end
437                        if g <= 0 then
438                            break
439                        end
440                    else
441                        break
442                    end
443                end
444                if g > 0 then
445                    readerror("left",n)
446                    s = ""
447                elseif g < 0 then
448                    readerror("right",n)
449                    s = ""
450                elseif l == 0 then
451                    if c:endoffile() then
452                        lastdone[n] = true
453                        s = "\\par"
454                    else
455                        s = ""
456                    end
457                    channels[n] = false
458                else
459                    local e = texgetcount("endlinechar") -- we can have tex.endline if needed
460                    if e < 0 or e > 127 then
461                        e = ""
462                    else
463                        e = " "
464                        l = l + 1
465                        t[l] = ""
466                    end
467                    s = concat(t, e, 1, l)
468                end
469            else
470                s = ""
471            end
472            setmacro(m, s, prefix) -- checks for frozen
473        end,
474    }
475
476    -- This is an etex extension. All characters become catcode 12 but a space gets code
477    -- 10. The etex manual specifies a lineending of catcode 12 too. Should we strip spaces
478    -- at the end? A bit weird command that we never use anyway.
479
480    implement {
481        name      = "readline",
482        public    = true,
483        usage     = "value",
484        actions   = function(prefix)
485            local n = scaninteger()
486            local c = channels[n]
487            if scankeyword("line") and c then
488                c:gotoline(scaninteger())
489            end
490            scankeyword("to")
491            local m = scancsname(true)
492            local s = c and c:reader()
493            if s then
494                local e = texgetcount("endlinechar") -- we can have tex.endline if needed
495                if e > 0 then -- forget about 0
496                    s = s .. char(e)
497                end
498            else
499                channels[n] = false
500                s = ""
501            end
502            setmacro(rlncatcodes, m, s, prefix) -- checks for frozen
503        end,
504    }
505
506    implement {
507        name      = "readlinedirect",
508        public    = true,
509        untraced  = true,
510        actions   = function(prefix)
511            local n = scaninteger()
512            local c = channels[n]
513            if scankeyword("line") and c then
514                c:gotoline(scaninteger())
515            end
516            local s = c and c:reader()
517            if s then
518                local e = texgetcount("endlinechar") -- we can have tex.endline if needed
519                if e > 0 then -- forget about 0
520                    s = s .. char(e)
521                end
522                context(s)
523            else
524                channels[n] = false
525            end
526        end,
527    }
528
529    -- This one uses the special lua condition option which is kind of experimental
530    -- but seems to work fine.
531
532    implement {
533        name      = "ifeof",
534        public    = true,
535        usage     = "condition",
536        actions   = function()
537            local n = scaninteger()
538            return boolean_value, not channels[n]
539        end,
540    }
541
542    -- This one doesn't belong here and it might become a real primitive if we need it
543    -- frequently. So, for the moment we keep it in this file.
544
545    local getnest = tex.getnest
546
547    implement {
548        name      = "ifmvl",
549        public    = true,
550        usage     = "condition",
551        actions   = function()
552            return boolean_value, getnest("ptr") == 0
553        end,
554    }
555
556end
557