luat-mac.lua /size: 17 Kb    last modification: 2021-10-28 13:50
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
186local name           = (R("AZ","az") + utf8character)^1
187local csname         = (R("AZ","az") + S("@?!_:-*") + utf8character)^1
188local longname       = (longleft/"") * (nolong^1) * (longright/"")
189local variable       = P("#") * Cs(name + longname)
190local bcsname        = P("csname")
191local ecsname        = escape * P("endcsname")
192local escapedname    = escape * csname
193local definer        = escape * (P("u")^-1 * S("egx")^-1 * P("def"))             -- tex
194local setter         = escape * P("set") * (P("u")^-1 * S("egx")^-1) * P("value") -- context specific
195---                  + escape * P("install") * (1-P("handler"))^1 * P("handler")  -- context specific
196local defcsname      = escape * S("egx")^-1 * P("defcsname")
197                     * (1 - ecsname)^1
198                     * ecsname
199local startcode      = P("\\starttexdefinition")                                  -- context specific
200local stopcode       = P("\\stoptexdefinition")                                   -- context specific
201local anything       = patterns.anything
202local always         = patterns.alwaysmatched
203
204
205-- The comment nilling can become an option but it nicely compensates the Lua
206-- parsing here with less parsing at the TeX end. We keep lines so the errors
207-- get reported all right, but comments are never seen there anyway. We keep
208-- comment that starts inline as it can be something special with a % (at some
209-- point we can do that as well, esp if we never use \% or `% somewhere
210-- unpredictable). We need to skip comments anyway. Hm, too tricky, this
211-- stripping as we can have Lua code etc.
212
213local commenttoken   = P("%")
214local crorlf         = S("\n\r")
215local commentline    = commenttoken * ((1-crorlf)^0)
216local leadingcomment = (commentline * crorlf^1)^1
217local furthercomment = (crorlf^1 * commentline)^1
218
219local pushlocal      = always   / push
220local poplocal       = always   / pop
221local declaration    = variable / set
222local identifier     = variable / get
223
224local argument       = P { leftbrace * ((identifier + V(1) + (1 - leftbrace - rightbrace))^0) * rightbrace }
225
226local function matcherror(str,pos)
227    report_macros("runaway definition at: %s",sub(str,pos-30,pos))
228end
229
230local csname_endcsname = P("\\csname") * (identifier + (1 - P("\\endcsname")))^1
231
232local grammar = { "converter",
233    texcode     = pushlocal
234                * startcode
235                * spaces
236                * (csname * spaces)^1 -- new: multiple, new:csname instead of name
237                * ((declaration * (space^0/""))^1 + furthercomment + (1 - newline - space))^0 -- accepts #a #b #c
238                * V("texbody")
239                * stopcode
240                * poplocal,
241    texbody     = (
242                      leadingcomment -- new per 2015-03-03 (ugly)
243                    + V("definition")
244                    + identifier
245                    + V("braced")
246                    + (1 - stopcode)
247                  )^0,
248    definition  = pushlocal
249                * (definer * spaces^0 * escapedname)
250                * (declaration + furthercomment + commentline + csname_endcsname + (1-leftbrace))^0
251                * V("braced")
252                * poplocal,
253    csnamedef   = pushlocal
254                * defcsname
255                * (declaration + furthercomment + commentline + csname_endcsname + (1-leftbrace))^0
256                * V("braced")
257                * poplocal,
258    setcode     = pushlocal
259                * setter
260                * argument
261                * (declaration + furthercomment + commentline + (1-leftbrace))^0
262                * V("braced")
263                * poplocal,
264    braced      = leftbrace
265                * (   V("definition")
266                    + identifier
267                    + V("setcode")
268                    + V("texcode")
269                    + V("braced")
270                    + furthercomment
271                    + leadingcomment -- new per 2012-05-15 (message on mailing list)
272                    + nobrace
273                  )^0
274                * (rightbrace + Cmt(always,matcherror)),
275
276    pattern     = leadingcomment
277                + V("definition")
278                + V("csnamedef")
279                + V("setcode")
280                + V("texcode")
281                + furthercomment
282                + anything,
283
284    converter   = V("pattern")^1,
285}
286
287local parser = Cs(grammar)
288
289local checker = P("%") * (1 - newline - P("macros"))^0
290              * P("macros") * space^0 * P("=") * space^0 * C(patterns.letter^1)
291
292-- maybe namespace
293
294local resolvers  = resolvers
295
296local macros     = { }
297resolvers.macros = macros
298
299local loadtexfile = resolvers.loadtexfile
300
301function macros.preprocessed(str,strip)
302    return lpegmatch(parser,str,1,strip)
303end
304
305function macros.convertfile(oldname,newname) -- beware, no testing on oldname == newname
306    local data = loadtexfile(oldname)
307    data = macros.preprocessed(data) or "" -- interfaces not yet defined
308    savedata(newname,data)
309end
310
311function macros.version(data)
312    return lpegmatch(checker,data)
313end
314
315-- the document variables hack is temporary
316
317local processors = { }
318
319function processors.mkvi(str,filename)
320    local oldsize = #str
321    str = lpegmatch(parser,str,1,true) or str
322    pushtarget("logfile")
323    report_macros("processed mkvi file %a, delta %s",filename,oldsize-#str)
324    poptarget()
325    return str
326end
327
328function processors.mkix(str,filename) -- we could intercept earlier so that caching works better
329    if not document then               -- because now we hash the string as well as the
330        document = { }
331    end
332    if not document.variables then
333        document.variables = { }
334    end
335    local oldsize = #str
336    str = convertlmxstring(str,document.variables,false) or str
337    pushtarget("logfile")
338    report_macros("processed mkix file %a, delta %s",filename,oldsize-#str)
339    poptarget()
340    return str
341end
342
343function processors.mkxi(str,filename)
344    if not document then
345        document = { }
346    end
347    if not document.variables then
348        document.variables = { }
349    end
350    local oldsize = #str
351    str = convertlmxstring(str,document.variables,false) or str
352    str = lpegmatch(parser,str,1,true) or str
353    pushtarget("logfile")
354    report_macros("processed mkxi file %a, delta %s",filename,oldsize-#str)
355    poptarget()
356    return str
357end
358
359processors.mklx = processors.mkvi
360processors.mkxl = processors.mkiv
361
362function macros.processmk(str,filename)
363    if filename then
364        local suffix = filesuffix(filename)
365        local processor = processors[suffix] or processors[lpegmatch(checker,str)]
366        if processor then
367            str = processor(str,filename)
368        end
369    end
370    return str
371end
372
373local function validvi(filename,str)
374    local suffix = filesuffix(filename)
375    if suffix == "mkvi" or suffix == "mklx" then
376        return true
377    else
378        local check = lpegmatch(checker,str)
379        return check == "mkvi" or check == "mklx"
380    end
381end
382
383function macros.processmkvi(str,filename)
384    if filename and filename ~= "" and validvi(filename,str) then
385        local oldsize = #str
386        str = lpegmatch(parser,str,1,true) or str
387        pushtarget("logfile")
388        report_macros("processed mkvi file %a, delta %s",filename,oldsize-#str)
389        poptarget()
390    end
391    return str
392end
393
394macros.processmklx = macros.processmkvi
395
396-- bonus
397
398local schemes = resolvers.schemes
399
400if schemes then
401
402    local function handler(protocol,name,cachename)
403        local hashed = url.hashed(name)
404        local path = hashed.path
405        if path and path ~= "" then
406            local str = loadtexfile(path)
407            if validvi(path,str) then
408                -- already done automatically
409                savedata(cachename,str)
410            else
411                local result = lpegmatch(parser,str,1,true) or str
412                pushtarget("logfile")
413                report_macros("processed scheme %a, delta %s",filename,#str-#result)
414                poptarget()
415                savedata(cachename,result)
416            end
417        end
418        return cachename
419    end
420
421    schemes.install('mkvi',handler,1)
422    schemes.install('mklx',handler,1)
423
424end
425
426-- print(macros.preprocessed(
427-- [[
428--     \starttexdefinition unexpanded test #aa #bb #cc
429--         test
430--     \stoptexdefinition
431-- ]]))
432
433-- print(macros.preprocessed([[\checked \def \bla #bla{bla#{bla}}]]))
434-- print(macros.preprocessed([[\checked \def \bla #bla#discard#foo{bla#{bla}+#ignore+bla#foo}]]))
435-- print(macros.preprocessed([[\checked \def \bla #bla#ignore#foo{bla#{bla}+#ignore+bla#foo}]]))
436-- print(macros.preprocessed([[\def\bla#bla{#{bla}bla}]]))
437-- print(macros.preprocessed([[\def\blä#{blá}{blà:#{blá}}]]))
438-- print(macros.preprocessed([[\def\blä#bla{blà:#bla}]]))
439-- print(macros.preprocessed([[\setvalue{xx}#bla{blà:#bla}]]))
440-- print(macros.preprocessed([[\def\foo#bar{\setvalue{xx#bar}{#bar}}]]))
441-- print(macros.preprocessed([[\def\bla#bla{bla:#{bla}}]]))
442-- print(macros.preprocessed([[\def\bla_bla#bla{bla:#bla}]]))
443-- print(macros.preprocessed([[\def\test#oeps{test:#oeps}]]))
444-- print(macros.preprocessed([[\def\test_oeps#oeps{test:#oeps}]]))
445-- print(macros.preprocessed([[\def\test#oeps{test:#{oeps}}]]))
446-- print(macros.preprocessed([[\def\test#{oeps:1}{test:#{oeps:1}}]]))
447-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps}]]))
448-- print(macros.preprocessed([[\def\x[#a][#b][#c]{\setvalue{\y{#a}\z{#b}}{#c}}]]))
449-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps \halign{##\cr #oeps\cr}]]))
450-- print(macros.preprocessed([[\def\test#{oeps}{test:#oeps \halign{##\cr #oeps\cr}}]]))
451-- print(macros.preprocessed([[% test
452-- \def\test#oeps{#oeps} % {test}
453-- % test
454--
455-- % test
456-- two
457-- %test]]))
458-- print(macros.preprocessed([[
459-- \def\scrn_button_make_normal#namespace#current#currentparameter#text%
460--   {\ctxlua{structures.references.injectcurrentset(nil,nil)}%
461-- %    \hbox attr \referenceattribute \lastreferenceattribute {\localframed[#namespace:#current]{#text}}}
462--    \hbox attr \referenceattribute \lastreferenceattribute {\directlocalframed[#namespace:#current]{#text}}}
463-- ]]))
464--
465-- print(macros.preprocessed([[
466-- \def\definefoo[#name]%
467--  {\setvalue{start#name}{\dostartfoo{#name}}}
468-- \def\dostartfoo#name%
469--   {\def\noexpand\next#content\expandafter\noexpand\csname stop#name\endcsname{#name : #content}%
470--   \next}
471-- \def\dostartfoo#name%
472--  {\normalexpanded{\def\noexpand\next#content\expandafter\noexpand\csname stop#name\endcsname}{#name : #content}%
473--   \next}
474-- ]]))
475--
476-- print(macros.preprocessed([[
477-- \def\dosomething#content{%%% {{
478--     % { }{{ %%
479--     \bgroup\italic#content\egroup
480--   }
481-- ]]))
482--
483-- print(macros.preprocessed([[
484-- \unexpanded\def\start#tag#stoptag%
485--   {\initialize{#tag}%
486--    \normalexpanded
487--      {\def\yes[#one]#two\csname\e!stop#stoptag\endcsname{\command_yes[#one]{#two}}%
488--       \def\nop      #one\csname\e!stop#stoptag\endcsname{\command_nop      {#one}}}%
489--    \doifelsenextoptional\yes\nop}
490-- ]]))
491--
492-- print(macros.preprocessed([[
493-- \normalexpanded{\long\def\expandafter\noexpand\csname\e!start\v!interactionmenu\endcsname[#tag]#content\expandafter\noexpand\csname\e!stop\v!interactionmenu\endcsname}%
494--   {\def\currentinteractionmenu{#tag}%
495--    \expandafter\settrue\csname\??menustate\interactionmenuparameter\c!category\endcsname
496--    \setinteractionmenuparameter\c!menu{#content}}
497-- ]]))
498--
499-- Just an experiment:
500--
501-- \catcode\numexpr"10FF25=\commentcatcode %% > 110000 is invalid
502--
503-- We could have a push/pop mechanism but binding to txtcatcodes
504-- is okay too.
505
506local txtcatcodes   = false -- also signal and yet unknown
507
508local commentsignal = utf.char(0x10FF25)
509
510local encodecomment = P("%%") / commentsignal --
511----- encodepattern = Cs(((1-encodecomment)^0 * encodecomment)) -- strips but not nice for verbatim
512local encodepattern = Cs((encodecomment + 1)^0)
513local decodecomment = P(commentsignal) / "%%%%" -- why doubles here?
514local decodepattern = Cs((decodecomment + 1)^0)
515
516function macros.encodecomment(str)
517    if txtcatcodes and tex.catcodetable == txtcatcodes then
518        return lpegmatch(encodepattern,str) or str
519    else
520        return str
521    end
522end
523
524function macros.decodecomment(str) -- normally not needed
525    return txtcatcodes and lpegmatch(decodepattern,str) or str
526end
527
528-- resolvers.macros.commentsignal        = commentsignal
529-- resolvers.macros.encodecommentpattern = encodepattern
530-- resolvers.macros.decodecommentpattern = decodepattern
531
532local sequencers   = utilities.sequencers
533local appendaction = sequencers and sequencers.appendaction
534
535if appendaction then
536
537    local textlineactions = resolvers.openers.helpers.textlineactions
538    local textfileactions = resolvers.openers.helpers.textfileactions
539
540    appendaction(textfileactions,"system","resolvers.macros.processmk")
541    appendaction(textfileactions,"system","resolvers.macros.processmkvi")
542
543    function macros.enablecomment(thecatcodes)
544        if not txtcatcodes then
545            txtcatcodes = thecatcodes or catcodes.numbers.txtcatcodes
546            appendaction(textlineactions,"system","resolvers.macros.encodecomment")
547        end
548    end
549
550end
551