luat-mac.lua /size: 18 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['luat-mac'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Sometimes we run into situations like:
10--
11-- \def\foo#1{\expandafter\def\csname#1\endcsname}
12--
13-- As this confuses the parser, the following should be used instead:
14--
15-- \def\foo#1{\expandafter\normaldef\csname#1\endcsname}
16
17local P, V, S, R, C, Cs, Cmt, Carg = lpeg.P, lpeg.V, lpeg.S, lpeg.R, lpeg.C, lpeg.Cs, lpeg.Cmt, lpeg.Carg
18local lpegmatch, patterns = lpeg.match, lpeg.patterns
19
20local insert, remove = table.insert, table.remove
21local rep, sub = string.rep, string.sub
22local setmetatable = setmetatable
23local filesuffix = file.suffix
24local convertlmxstring = lmx and lmx.convertstring
25local savedata = io.savedata
26
27local pushtarget, poptarget = logs.pushtarget, logs.poptarget
28
29local report_macros = logs.reporter("interface","macros")
30
31local stack, top, n, hashes = { }, nil, 0, { }
32
33-- local function set(s)
34--     if top then
35--         n = n + 1
36--         if n > 9 then
37--             report_macros("number of arguments > 9, ignoring %s",s)
38--         else
39--             local ns = #stack
40--             local h = hashes[ns]
41--             if not h then
42--                 h = rep("#",2^(ns-1))
43--                 hashes[ns] = h
44--             end
45--             if s == "ignore" then
46--                 top[s] = ""
47--                 return h .. "0"
48--             else
49--                 local m = h .. n
50--                 top[s] = m
51--                 return m
52--             end
53--         end
54--     end
55-- end
56
57-- local function get(s)
58--     if s == "ignore" then
59--         return ""
60--     else
61--         if not top then
62--             report_macros("keeping #%s, no stack",s)
63--             return "#" .. s -- can be lua
64--         end
65--         local m = top[s]
66--         if m then
67--             return m
68--         else
69--             report_macros("keeping #%s, not on stack",s)
70--             return "#" .. s -- quite likely an error
71--         end
72--     end
73-- end
74
75local set = CONTEXTLMTXMODE > 0 and
76    function(s)
77        if top then
78            local ns = #stack
79            local h = hashes[ns]
80            if not h then
81                h = rep("#",2^(ns-1))
82                hashes[ns] = h
83            end
84            if s == "ignore" then
85                return h .. "-"
86            elseif s == "spacer" then
87                return h .. "*"
88            elseif s == "keepspacer" then
89                return h .. ","
90            elseif s == "pickup" then
91                return h .. ":"
92            else
93                n = n + 1
94                if n > 9 then
95                    report_macros("number of arguments > 9, ignoring %s",s)
96                elseif s == "discard" then
97                    top[s] = ""
98                    return h .. "0"
99                elseif s == "keepbraces" then
100                    top[s] = ""
101                    return h .. "+"
102                elseif s == "mandate" then
103                    top[s] = ""
104                    return h .. "="
105                elseif s == "keepmandate" then
106                    top[s] = ""
107                    return h .. "_"
108                elseif s == "prunespacing" then
109                    top[s] = ""
110                    return h .. "/"
111                else
112                    local m = h .. n
113                    top[s] = m
114                    return m
115                end
116            end
117        end
118    end
119or
120    function(s)
121        if top then
122            local ns = #stack
123            local h = hashes[ns]
124            if not h then
125                h = rep("#",2^(ns-1))
126                hashes[ns] = h
127            end
128            n = n + 1
129            if n > 9 then
130                report_macros("number of arguments > 9, ignoring %s",s)
131            else
132                local m = h .. n
133                top[s] = m
134                return m
135            end
136        end
137    end
138
139local function get(s)
140    if s == "ignore" or s == "discard" then
141        return ""
142    else
143        if not top then
144            report_macros("keeping #%s, no stack",s)
145            return "#" .. s -- can be lua
146        end
147        local m = top[s]
148        if m then
149            return m
150        else
151            report_macros("keeping #%s, not on stack",s)
152            return "#" .. s -- quite likely an error
153        end
154    end
155end
156
157local function push()
158    top = { }
159    n = 0
160    local s = stack[#stack]
161    if s then
162        setmetatable(top,{ __index = s })
163    end
164    insert(stack,top)
165end
166
167local function pop()
168    top = remove(stack)
169end
170
171local leftbrace      = P("{")   -- will be in patterns
172local rightbrace     = P("}")
173local escape         = P("\\")
174
175local space          = patterns.space
176local spaces         = space^1
177local newline        = patterns.newline
178local nobrace        = 1 - leftbrace - rightbrace
179
180local longleft       = leftbrace  -- P("(")
181local longright      = rightbrace -- P(")")
182local nolong         = 1 - longleft - longright
183
184local utf8character  = P(1) * R("\128\191")^1 -- unchecked but fast
185
186-- so no #[AZ] permitted
187
188local name           = ((R("az")      + utf8character) * (R("AZ","az") + utf8character)^0)
189                     + ((R("AZ","az") + utf8character) * (R("AZ","az") + utf8character)^1)
190local csname         = (R("AZ","az") + S("@?!_:-*") + utf8character)^1
191local longname       = (longleft/"") * (nolong^1) * (longright/"")
192local variable       = P("#") * Cs(name + longname)
193local bcsname        = P("csname")
194local ecsname        = escape * P("endcsname")
195local escapedname    = escape * csname
196local definer        = escape * (P("u")^-1 * S("egx")^-1 * P("def"))             -- tex
197local setter         = escape * P("set") * (P("u")^-1 * S("egx")^-1) * P("value") -- context specific
198---                  + escape * P("install") * (1-P("handler"))^1 * P("handler")  -- context specific
199local defcsname      = escape * S("egx")^-1 * P("defcsname")
200                     * (1 - ecsname)^1
201                     * ecsname
202local startcode      = P("\\starttexdefinition")                                  -- context specific
203local stopcode       = P("\\stoptexdefinition")                                   -- context specific
204local anything       = patterns.anything
205local always         = patterns.alwaysmatched
206
207
208-- The comment nilling can become an option but it nicely compensates the Lua
209-- parsing here with less parsing at the TeX end. We keep lines so the errors
210-- get reported all right, but comments are never seen there anyway. We keep
211-- comment that starts inline as it can be something special with a % (at some
212-- point we can do that as well, esp if we never use \% or `% somewhere
213-- unpredictable). We need to skip comments anyway. Hm, too tricky, this
214-- stripping as we can have Lua code etc.
215
216local commenttoken   = P("%")
217local crorlf         = S("\n\r")
218local commentline    = commenttoken * ((1-crorlf)^0)
219local leadingcomment = (commentline * crorlf^1)^1
220local furthercomment = (crorlf^1 * commentline)^1
221
222local pushlocal      = always   / push
223local poplocal       = always   / pop
224local declaration    = variable / set
225local identifier     = variable / get
226
227local argument       = P { leftbrace * ((identifier + V(1) + (1 - leftbrace - rightbrace))^0) * rightbrace }
228
229local function matcherror(str,pos)
230    report_macros("runaway definition at: %s",sub(str,pos-30,pos))
231    os.exit()
232end
233
234local csname_endcsname = P("\\csname") * (identifier + (1 - P("\\endcsname")))^1
235
236local grammar = { "converter",
237    texcode     = pushlocal
238                * startcode
239                * spaces
240                * (csname * spaces)^1 -- new: multiple, new:csname instead of name
241                * ((declaration * (space^0/""))^1 + furthercomment + (1 - newline - space))^0 -- accepts #a #b #c
242                * V("texbody")
243                * stopcode
244                * poplocal,
245    texbody     = (
246                      leadingcomment -- new per 2015-03-03 (ugly)
247                    + V("definition")
248                    + identifier
249                    + V("braced")
250                    + (1 - stopcode)
251                  )^0,
252    definition  = pushlocal
253                * (definer * spaces^0 * escapedname)
254                * (declaration + furthercomment + commentline + csname_endcsname + (1-leftbrace))^0
255                * V("braced")
256                * poplocal,
257    csnamedef   = pushlocal
258                * defcsname
259                * (declaration + furthercomment + commentline + csname_endcsname + (1-leftbrace))^0
260                * V("braced")
261                * poplocal,
262    setcode     = pushlocal
263                * setter
264                * argument
265                * (declaration + furthercomment + commentline + (1-leftbrace))^0
266                * V("braced")
267                * poplocal,
268    braced      = leftbrace
269                * (   V("definition")
270                    + identifier
271                    + V("setcode")
272                    + V("texcode")
273                    + V("braced")
274                    + furthercomment
275                    + leadingcomment -- new per 2012-05-15 (message on mailing list)
276                    + nobrace
277                  )^0
278                * (rightbrace + Cmt(always,matcherror)),
279
280    pattern     = leadingcomment
281                + V("definition")
282                + V("csnamedef")
283                + V("setcode")
284                + V("texcode")
285                + furthercomment
286                + anything,
287
288    converter   = V("pattern")^1,
289}
290
291local parser = Cs(grammar)
292
293local checker = P("%") * (1 - newline - P("macros"))^0
294              * P("macros") * space^0 * P("=") * space^0 * C(patterns.letter^1)
295
296-- maybe namespace
297
298local resolvers  = resolvers
299
300local macros     = { }
301resolvers.macros = macros
302
303local loadtexfile = resolvers.loadtexfile
304
305function macros.preprocessed(str,strip)
306    return lpegmatch(parser,str,1,strip)
307end
308
309function macros.convertfile(oldname,newname) -- beware, no testing on oldname == newname
310    local data = (loadtexfile or io.loaddata)(oldname)
311    data = macros.preprocessed(data) or "" -- interfaces not yet defined
312    savedata(newname,data)
313end
314
315-- macros.convertfile("c:/data/develop/context/sources/publ-imp-definitions.mkvi","e:/tmp/temp-macros.mkiv")
316
317function macros.version(data)
318    return lpegmatch(checker,data)
319end
320
321-- the document variables hack is temporary
322
323local processors = { }
324
325function processors.mkvi(str,filename)
326    local oldsize = #str
327    str = lpegmatch(parser,str,1,true) or str
328    pushtarget("logfile")
329    report_macros("processed mkvi file %a, delta %s",filename,oldsize-#str)
330    poptarget()
331    return str
332end
333
334function processors.mkix(str,filename) -- we could intercept earlier so that caching works better
335    if not document then               -- because now we hash the string as well as the
336        document = { }
337    end
338    if not document.variables then
339        document.variables = { }
340    end
341    local oldsize = #str
342    str = convertlmxstring(str,document.variables,false) or str
343    pushtarget("logfile")
344    report_macros("processed mkix file %a, delta %s",filename,oldsize-#str)
345    poptarget()
346    return str
347end
348
349function processors.mkxi(str,filename)
350    if not document then
351        document = { }
352    end
353    if not document.variables then
354        document.variables = { }
355    end
356    local oldsize = #str
357    str = convertlmxstring(str,document.variables,false) or str
358    str = lpegmatch(parser,str,1,true) or str
359    pushtarget("logfile")
360    report_macros("processed mkxi file %a, delta %s",filename,oldsize-#str)
361    poptarget()
362    return str
363end
364
365processors.mklx = processors.mkvi
366processors.mkxl = processors.mkiv
367
368function macros.processmk(str,filename)
369    if filename then
370        local suffix = filesuffix(filename)
371        local processor = processors[suffix] or processors[lpegmatch(checker,str)]
372        if processor then
373            str = processor(str,filename)
374        end
375    end
376    return str
377end
378
379local function validvi(filename,str)
380    local suffix = filesuffix(filename)
381    if suffix == "mkvi" or suffix == "mklx" then
382        return true
383    else
384        local check = lpegmatch(checker,str)
385        return check == "mkvi" or check == "mklx"
386    end
387end
388
389function macros.processmkvi(str,filename)
390    if filename and filename ~= "" and validvi(filename,str) then
391        local oldsize = #str
392        str = lpegmatch(parser,str,1,true) or str
393        pushtarget("logfile")
394        report_macros("processed mkvi file %a, delta %s",filename,oldsize-#str)
395        poptarget()
396    end
397    return str
398end
399
400macros.processmklx = macros.processmkvi
401
402-- bonus
403
404local schemes = resolvers.schemes
405
406if schemes then
407
408    local function handler(protocol,name,cachename)
409        local hashed = url.hashed(name)
410        local path = hashed.path
411        if path and path ~= "" then
412            local str = loadtexfile(path)
413            if validvi(path,str) then
414                -- already done automatically
415                savedata(cachename,str)
416            else
417                local result = lpegmatch(parser,str,1,true) or str
418                pushtarget("logfile")
419                report_macros("processed scheme %a, delta %s",filename,#str-#result)
420                poptarget()
421                savedata(cachename,result)
422            end
423        end
424        return cachename
425    end
426
427    schemes.install('mkvi',handler,1)
428    schemes.install('mklx',handler,1)
429
430end
431
432-- print(macros.preprocessed(
433-- [[
434--     \starttexdefinition unexpanded test #aa #bb #cc
435--         test
436--     \stoptexdefinition
437-- ]]))
438
439-- print(macros.preprocessed([[\checked \def \bla #bla{bla#{bla}}]]))
440-- print(macros.preprocessed([[\checked \def \bla #bla#discard#foo{bla#{bla}+#ignore+bla#foo}]]))
441-- print(macros.preprocessed([[\checked \def \bla #bla#ignore#foo{bla#{bla}+#ignore+bla#foo}]]))
442-- print(macros.preprocessed([[\def\bla#bla{#{bla}bla}]]))
443-- print(macros.preprocessed([[\def\blä#{blá}{blà:#{blá}}]]))
444-- print(macros.preprocessed([[\def\blä#bla{blà:#bla}]]))
445-- print(macros.preprocessed([[\setvalue{xx}#bla{blà:#bla}]]))
446-- print(macros.preprocessed([[\def\foo#bar{\setvalue{xx#bar}{#bar}}]]))
447-- print(macros.preprocessed([[\def\bla#bla{bla:#{bla}}]]))
448-- print(macros.preprocessed([[\def\bla_bla#bla{bla:#bla}]]))
449-- print(macros.preprocessed([[\def\test#oeps{test:#oeps}]]))
450-- print(macros.preprocessed([[\def\test_oeps#oeps{test:#oeps}]]))
451-- print(macros.preprocessed([[\def\test#oeps{test:#{oeps}}]]))
452-- print(macros.preprocessed([[\def\test#{oeps:1}{test:#{oeps:1}}]]))
453-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps}]]))
454-- print(macros.preprocessed([[\def\x[#a][#b][#c]{\setvalue{\y{#a}\z{#b}}{#c}}]]))
455-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps \halign{##\cr #oeps\cr}]]))
456-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps \halign{##\cr #oeps\cr}}]]))
457-- print(macros.preprocessed([[% test
458-- \def\test#oeps{#oeps} % {test}
459-- % test
460--
461-- % test
462-- two
463-- %test]]))
464-- print(macros.preprocessed([[
465-- \def\scrn_button_make_normal#namespace#current#currentparameter#text%
466--   {\ctxlua{structures.references.injectcurrentset(nil,nil)}%
467-- %    \hbox attr \referenceattribute \lastreferenceattribute {\localframed[#namespace:#current]{#text}}}
468--    \hbox attr \referenceattribute \lastreferenceattribute {\directlocalframed[#namespace:#current]{#text}}}
469-- ]]))
470--
471-- print(macros.preprocessed([[
472-- \def\definefoo[#name]%
473--  {\setvalue{start#name}{\dostartfoo{#name}}}
474-- \def\dostartfoo#name%
475--   {\def\noexpand\next#content\expandafter\noexpand\csname stop#name\endcsname{#name : #content}%
476--   \next}
477-- \def\dostartfoo#name%
478--  {\normalexpanded{\def\noexpand\next#content\expandafter\noexpand\csname stop#name\endcsname}{#name : #content}%
479--   \next}
480-- ]]))
481--
482-- print(macros.preprocessed([[
483-- \def\dosomething#content{%%% {{
484--     % { }{{ %%
485--     \bgroup\italic#content\egroup
486--   }
487-- ]]))
488--
489-- print(macros.preprocessed([[
490-- \unexpanded\def\start#tag#stoptag%
491--   {\initialize{#tag}%
492--    \normalexpanded
493--      {\def\yes[#one]#two\csname\e!stop#stoptag\endcsname{\command_yes[#one]{#two}}%
494--       \def\nop      #one\csname\e!stop#stoptag\endcsname{\command_nop      {#one}}}%
495--    \doifelsenextoptional\yes\nop}
496-- ]]))
497--
498-- print(macros.preprocessed([[
499-- \normalexpanded{\long\def\expandafter\noexpand\csname\e!start\v!interactionmenu\endcsname[#tag]#content\expandafter\noexpand\csname\e!stop\v!interactionmenu\endcsname}%
500--   {\def\currentinteractionmenu{#tag}%
501--    \expandafter\settrue\csname\??menustate\interactionmenuparameter\c!category\endcsname
502--    \setinteractionmenuparameter\c!menu{#content}}
503-- ]]))
504--
505-- Just an experiment:
506--
507-- \catcode\numexpr"10FF25=\commentcatcode %% > 110000 is invalid
508--
509-- We could have a push/pop mechanism but binding to txtcatcodes
510-- is okay too.
511
512local txtcatcodes   = false -- also signal and yet unknown
513
514local commentsignal = utf.char(0x10FF25)
515
516local encodecomment = P("%%") / commentsignal --
517----- encodepattern = Cs(((1-encodecomment)^0 * encodecomment)) -- strips but not nice for verbatim
518local encodepattern = Cs((encodecomment + 1)^0)
519local decodecomment = P(commentsignal) / "%%%%" -- why doubles here?
520local decodepattern = Cs((decodecomment + 1)^0)
521
522function macros.encodecomment(str)
523    if txtcatcodes and tex.catcodetable == txtcatcodes then
524        return lpegmatch(encodepattern,str) or str
525    else
526        return str
527    end
528end
529
530function macros.decodecomment(str) -- normally not needed
531    return txtcatcodes and lpegmatch(decodepattern,str) or str
532end
533
534-- resolvers.macros.commentsignal        = commentsignal
535-- resolvers.macros.encodecommentpattern = encodepattern
536-- resolvers.macros.decodecommentpattern = decodepattern
537
538local sequencers   = utilities.sequencers
539local appendaction = sequencers and sequencers.appendaction
540
541if appendaction then
542
543    local textlineactions = resolvers.openers.helpers.textlineactions
544    local textfileactions = resolvers.openers.helpers.textfileactions
545
546    appendaction(textfileactions,"system","resolvers.macros.processmk")
547    appendaction(textfileactions,"system","resolvers.macros.processmkvi")
548
549    function macros.enablecomment(thecatcodes)
550        if not txtcatcodes then
551            txtcatcodes = thecatcodes or catcodes.numbers.txtcatcodes
552            appendaction(textlineactions,"system","resolvers.macros.encodecomment")
553        end
554    end
555
556end
557