trac-lmx.lua /size: 20 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['trac-lmx'] = {
2    version   = 1.002,
3    comment   = "companion to trac-lmx.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-- This one will be adpated to the latest helpers. It might even become a
10-- module instead.
11
12local type, tostring, rawget, loadstring, pcall = type, tostring, rawget, loadstring, pcall
13local format, sub, gsub = string.format, string.sub, string.gsub
14local concat = table.concat
15local collapsespaces = string.collapsespaces
16local P, Cc, Cs, C, Carg, lpegmatch = lpeg.P, lpeg.Cc, lpeg.Cs, lpeg.C, lpeg.Carg, lpeg.match
17local joinpath, replacesuffix, pathpart, filesuffix = file.join, file.replacesuffix, file.pathpart, file.suffix
18
19local allocate          = utilities.storage.allocate
20local setmetatableindex = table.setmetatableindex
21
22----- trace_templates   = false  trackers  .register("lmx.templates",      function(v) trace_templates = v end)
23local trace_variables   = false  trackers  .register("lmx.variables",      function(v) trace_variables = v end)
24
25local cache_templates   = true   directives.register("lmx.cache.templates",function(v) cache_templates = v end)
26local cache_files       = true   directives.register("lmx.cache.files",    function(v) cache_files     = v end)
27
28local report_lmx        = logs.reporter("lmx")
29local report_error      = logs.reporter("lmx","error")
30
31lmx                     = lmx or { }
32local lmx               = lmx
33
34-- This will change: we will just pass the global defaults as argument, but then we need
35-- to rewrite some older code or come up with an ugly trick.
36
37local lmxvariables = {
38    ['title-default']           = 'ConTeXt LMX File',
39    ['color-background-green']  = '#4F6F6F',
40    ['color-background-blue']   = '#6F6F8F',
41    ['color-background-yellow'] = '#8F8F6F',
42    ['color-background-purple'] = '#8F6F8F',
43    ['color-background-body']   = '#808080',
44    ['color-background-main']   = '#3F3F3F',
45}
46
47local lmxinherited = {
48    ['title']                   = 'title-default',
49    ['color-background-one']    = 'color-background-green',
50    ['color-background-two']    = 'color-background-blue',
51    ['color-background-three']  = 'color-background-one',
52    ['color-background-four']   = 'color-background-two',
53}
54
55lmx.variables = lmxvariables
56lmx.inherited = lmxinherited
57
58setmetatableindex(lmxvariables,function(t,k)
59    k = lmxinherited[k]
60    while k do
61        local v = rawget(lmxvariables,k)
62        if v then
63            return v
64        end
65        k = lmxinherited[k]
66    end
67end)
68
69function lmx.set(key,value)
70    lmxvariables[key] = value
71end
72
73function lmx.get(key)
74    return lmxvariables[key] or ""
75end
76
77lmx.report = report_lmx
78
79-- helpers
80
81-- the variables table is an empty one that gets linked to a defaults table
82-- that gets passed with a creation (first time only) and that itself links
83-- to one that gets passed to the converter
84
85local variables = { }  -- we assume no nesting
86local result    = { }  -- we assume no nesting
87
88local function do_print(one,two,...)
89    if two then
90        result[#result+1] = concat { one, two, ... }
91    else
92        result[#result+1] = one
93    end
94end
95
96-- Although it does not make much sense for most elements, we provide a mechanism
97-- to print wrapped content, something that is more efficient when we are constructing
98-- tables.
99
100local html = { }
101lmx.html   = html
102
103function html.td(str)
104    if type(str) == "table" then
105        for i=1,#str do -- spoils t !
106            str[i] = format("<td>%s</td>",str[i] or "")
107        end
108        result[#result+1] = concat(str)
109    else
110        result[#result+1] = format("<td>%s</td>",str or "")
111    end
112end
113
114function html.th(str)
115    if type(str) == "table" then
116        for i=1,#str do -- spoils t !
117            str[i] = format("<th>%s</th>",str[i])
118        end
119        result[#result+1] = concat(str)
120    else
121        result[#result+1] = format("<th>%s</th>",str or "")
122    end
123end
124
125function html.a(text,url)
126    result[#result+1] = format("<a href=%q>%s</a>",url,text)
127end
128
129setmetatableindex(html,function(t,k)
130    local f = format("<%s>%%s</%s>",k,k)
131    local v = function(str) result[#result+1] = format(f,str or "") end
132    t[k] = v
133    return v
134end)
135
136-- Loading templates:
137
138local function loadedfile(name)
139    name = resolvers and resolvers.findfile and resolvers.findfile(name) or name
140    local data = io.loaddata(name)
141    if not data or data == "" then
142        report_lmx("file %a is empty",name)
143    end
144    return data
145end
146
147local function loadedsubfile(name)
148    return io.loaddata(resolvers and resolvers.findfile and resolvers.findfile(name) or name)
149end
150
151lmx.loadedfile = loadedfile
152
153-- A few helpers (the next one could end up in l-lpeg):
154
155local usedpaths = { }
156local givenpath = nil
157
158local do_nested_include = nil
159
160local pattern = lpeg.replacer {
161    ["&"] = "&amp;",
162    [">"] = "&gt;",
163    ["<"] = "&lt;",
164    ['"'] = "&quot;",
165}
166
167local function do_escape(str)
168    return lpegmatch(pattern,str) or str
169end
170
171local function do_variable(str)
172    local value = variables[str]
173    if not trace_variables then
174        -- nothing
175    elseif type(value) == "string" then
176        if #value > 80 then
177            report_lmx("variable %a is set to: %s ...",str,collapsespaces(sub(value,1,80)))
178        else
179            report_lmx("variable %a is set to: %s",str,collapsespaces(value))
180        end
181    elseif type(value) == "nil" then
182        report_lmx("variable %a is set to: %s",str,"<!-- unset -->")
183    else
184        report_lmx("variable %a is set to: %S",str,value)
185    end
186    if type(value) == "function" then -- obsolete ... will go away
187        return value(str)
188    else
189        return value
190    end
191end
192
193local function do_type(str)
194    if str and str ~= "" then
195        result[#result+1] = format("<tt>%s</tt>",do_escape(str))
196    end
197end
198
199local function do_fprint(str,...)
200    if str and str ~= "" then
201        result[#result+1] = format(str,...)
202    end
203end
204
205local function do_eprint(str,...)
206    if str and str ~= "" then
207        result[#result+1] = lpegmatch(pattern,format(str,...))
208    end
209end
210
211local function do_print_variable(str)
212    local str = do_variable(str) -- variables[str]
213    if str and str ~= "" then
214        result[#result+1] = str
215    end
216end
217
218local function do_type_variable(str)
219    local str = do_variable(str) -- variables[str]
220    if str and str ~= "" then
221        result[#result+1] = format("<tt>%s</tt>",do_escape(str))
222    end
223end
224
225local function do_include(filename,option)
226    local data = loadedsubfile(filename)
227    if (not data or data == "") and givenpath then
228        data = loadedsubfile(joinpath(givenpath,filename))
229    end
230    if (not data or data == "") and type(usedpaths) == "table" then
231        for i=1,#usedpaths do
232            data = loadedsubfile(joinpath(usedpaths[i],filename))
233            if data and data ~= "" then
234                break
235            end
236        end
237    end
238    if not data or data == "" then
239        data = format("<!-- unknown lmx include file: %s -->",filename)
240        report_lmx("include file %a is empty",filename)
241    else
242     -- report_lmx("included file: %s",filename)
243        data = do_nested_include(data)
244    end
245    if filesuffix(filename,"css") and option == "strip" then -- new
246        data = lmx.stripcss(data)
247    end
248    return data
249end
250
251-- Flushers:
252
253lmx.print     = do_print
254lmx.type      = do_type
255lmx.eprint    = do_eprint
256lmx.fprint    = do_fprint
257
258lmx.escape    = do_escape
259lmx.urlescape = url.escape
260lmx.variable  = do_variable
261lmx.include   = do_include
262
263lmx.inject    = do_print
264lmx.finject   = do_fprint
265lmx.einject   = do_eprint
266
267lmx.pv        = do_print_variable
268lmx.tv        = do_type_variable
269
270-- The next functions set up the closure.
271
272function lmx.initialize(d,v)
273    if not v then
274        setmetatableindex(d,lmxvariables)
275        if variables ~= d then
276            setmetatableindex(variables,d)
277            if trace_variables then
278                report_lmx("using chain: variables => given defaults => lmx variables")
279            end
280        elseif trace_variables then
281            report_lmx("using chain: variables == given defaults => lmx variables")
282        end
283    elseif d ~= v then
284        setmetatableindex(v,d)
285        if d ~= lmxvariables then
286            setmetatableindex(d,lmxvariables)
287            if variables ~= v then
288                setmetatableindex(variables,v)
289                if trace_variables then
290                    report_lmx("using chain: variables => given variables => given defaults => lmx variables")
291                end
292            elseif trace_variables then
293                report_lmx("using chain: variables == given variables => given defaults => lmx variables")
294            end
295        else
296            if variables ~= v then
297                setmetatableindex(variables,v)
298                if trace_variables then
299                    report_lmx("using chain: variabes => given variables => given defaults")
300                end
301            elseif trace_variables then
302                report_lmx("using chain: variables == given variables => given defaults")
303            end
304        end
305    else
306        setmetatableindex(v,lmxvariables)
307        if variables ~= v then
308            setmetatableindex(variables,v)
309            if trace_variables then
310                report_lmx("using chain: variables => given variables => lmx variables")
311            end
312        elseif trace_variables then
313            report_lmx("using chain: variables == given variables => lmx variables")
314        end
315    end
316    result = { }
317end
318
319function lmx.finalized()
320    local collapsed = concat(result)
321    result = { } -- free memory
322    return collapsed
323end
324
325function lmx.getvariables()
326    return variables
327end
328
329function lmx.reset()
330    -- obsolete
331end
332
333-- Creation: (todo: strip <!-- -->)
334
335-- local template = [[
336-- return function(defaults,variables)
337--
338-- -- initialize
339--
340-- lmx.initialize(defaults,variables)
341--
342-- -- interface
343--
344-- local definitions = { }
345-- local variables   = lmx.getvariables()
346-- local html        = lmx.html
347-- local inject      = lmx.print
348-- local finject     = lmx.fprint
349-- local einject     = lmx.eprint
350-- local escape      = lmx.escape
351-- local verbose     = lmx.type
352--
353-- -- shortcuts (sort of obsolete as there is no gain)
354--
355-- local p  = lmx.print
356-- local f  = lmx.fprint
357-- local v  = lmx.variable
358-- local e  = lmx.escape
359-- local t  = lmx.type
360-- local pv = lmx.pv
361-- local tv = lmx.tv
362--
363-- -- generator
364--
365-- %s
366--
367-- -- finalize
368--
369-- return lmx.finalized()
370--
371-- end
372-- ]]
373
374local template = [[
375-- interface
376
377local html        = lmx.html
378local inject      = lmx.print
379local finject     = lmx.fprint -- better use the following
380local einject     = lmx.eprint -- better use the following
381local injectf     = lmx.fprint
382local injecte     = lmx.eprint
383local injectfmt   = lmx.fprint
384local injectesc   = lmx.eprint
385local escape      = lmx.escape
386local verbose     = lmx.type
387
388local i_n_j_e_c_t = lmx.print
389
390-- shortcuts (sort of obsolete as there is no gain)
391
392local p  = lmx.print
393local f  = lmx.fprint
394local v  = lmx.variable
395local e  = lmx.escape
396local t  = lmx.type
397local pv = lmx.pv
398local tv = lmx.tv
399
400local lmx_initialize   = lmx.initialize
401local lmx_finalized    = lmx.finalized
402local lmx_getvariables = lmx.getvariables
403
404-- generator
405
406return function(defaults,variables)
407
408    lmx_initialize(defaults,variables)
409
410    local definitions = { }
411    local variables   = lmx_getvariables()
412
413    %s -- the action: appends to result
414
415    return lmx_finalized()
416
417end
418]]
419
420local function savedefinition(definitions,tag,content)
421    definitions[tag] = content
422    return ""
423end
424
425local function getdefinition(definitions,tag)
426    return definitions[tag] or ""
427end
428
429local whitespace     = lpeg.patterns.whitespace
430local optionalspaces = whitespace^0
431
432local dquote         = P('"')
433
434local begincomment   = P("<!--") -- only makes sense when we in intercept pre|script|style
435local endcomment     = P("-->")
436
437local beginembedxml  = P("<?")
438local endembedxml    = P("?>")
439
440local beginembedcss  = P("/*")
441local endembedcss    = P("*/")
442
443local gobbledendxml  = (optionalspaces * endembedxml) / ""
444----- argumentxml    = (1-gobbledendxml)^0
445local argumentxml    = (whitespace^1 + dquote * C((1-dquote)^1) * dquote + C((1-gobbledendxml-whitespace)^1))^0
446
447local gobbledendcss  = (optionalspaces * endembedcss) / ""
448----- argumentcss    = (1-gobbledendcss)^0
449local argumentcss    = (whitespace^1 + dquote * C((1-dquote)^1) * dquote + C((1-gobbledendcss-whitespace)^1))^0
450
451local commentxml     = (begincomment * (1-endcomment)^0 * endcomment) / ""
452
453local beginluaxml    = (beginembedxml * P("lua")) / ""
454local endluaxml      = endembedxml / ""
455
456local luacodexml     = beginluaxml
457                     * (1-endluaxml)^1
458                     * endluaxml
459
460local beginluacss    = (beginembedcss * P("lua")) / ""
461local endluacss      = endembedcss / ""
462
463local luacodecss     = beginluacss
464                     * (1-endluacss)^1
465                     * endluacss
466
467local othercode      = (1-beginluaxml-beginluacss)^1 / " i_n_j_e_c_t[==[%0]==] "
468
469local includexml     = ((beginembedxml * P("lmx-include") * optionalspaces) / "")
470                     * (argumentxml / do_include)
471                     * gobbledendxml
472
473local includecss     = ((beginembedcss * P("lmx-include") * optionalspaces) / "")
474                     * (argumentcss / do_include)
475                     * gobbledendcss
476
477local definexml_b    = ((beginembedxml * P("lmx-define-begin") * optionalspaces) / "")
478                     * argumentxml
479                     * gobbledendxml
480
481local definexml_e    = ((beginembedxml * P("lmx-define-end") * optionalspaces) / "")
482                     * argumentxml
483                     * gobbledendxml
484
485local definexml_c    = C((1-definexml_e)^0)
486
487local definexml      = (Carg(1) * C(definexml_b) * definexml_c * definexml_e) / savedefinition
488
489local resolvexml     = ((beginembedxml * P("lmx-resolve") * optionalspaces) / "")
490                     * ((Carg(1) * C(argumentxml)) / getdefinition)
491                     * gobbledendxml
492
493local definecss_b    = ((beginembedcss * P("lmx-define-begin") * optionalspaces) / "")
494                     * argumentcss
495                     * gobbledendcss
496
497local definecss_e    = ((beginembedcss * P("lmx-define-end") * optionalspaces) / "")
498                     * argumentcss
499                     * gobbledendcss
500
501local definecss_c    = C((1-definecss_e)^0)
502
503local definecss      = (Carg(1) * C(definecss_b) * definecss_c * definecss_e) / savedefinition
504
505local resolvecss     = ((beginembedcss * P("lmx-resolve") * optionalspaces) / "")
506                     * ((Carg(1) * C(argumentcss)) / getdefinition)
507                     * gobbledendcss
508
509----- pattern_1      = Cs((commentxml + includexml + includecss + P(1))^0) -- get rid of xml comments asap .. not good enough: embedded css and script is in <!--  .. also <pre>
510local pattern_1      = Cs((includexml + includecss + P(1))^0)
511local pattern_2      = Cs((definexml + resolvexml + definecss + resolvecss + P(1))^0)
512local pattern_3      = Cs((luacodexml + luacodecss + othercode)^0)
513
514local cache = { }
515
516local function lmxerror(str)
517    report_error(str)
518    return html.tt(str)
519end
520
521local function wrapper(converter,defaults,variables)
522    local outcome, message = pcall(converter,defaults,variables)
523    if not outcome then
524        return lmxerror(format("error in conversion: %s",message))
525    else
526        return message
527    end
528end
529
530do_nested_include = function(data) -- also used in include
531    return lpegmatch(pattern_1,data)
532end
533
534local function lmxnew(data,defaults,nocache,path) -- todo: use defaults in calling routines
535    data = data or ""
536    local known = cache[data]
537    if not known then
538        givenpath = path
539        usedpaths = lmxvariables.includepath or { }
540        if type(usedpaths) == "string" then
541            usedpaths = { usedpaths }
542        end
543        data = lpegmatch(pattern_1,data)
544        data = lpegmatch(pattern_2,data,1,{})
545        data = lpegmatch(pattern_3,data)
546        local converted = loadstring(format(template,data))
547        if converted then
548            converted = converted()
549        end
550        defaults = defaults or { }
551        local converter
552        if converted then
553            converter = function(variables)
554                return wrapper(converted,defaults,variables)
555            end
556        else
557            report_error("error in:\n%s\n:",data)
558            converter = function() lmxerror("error in template") end
559        end
560        known = {
561            data      = defaults.trace and data or "",
562            variables = defaults,
563            converter = converter,
564        }
565        if cache_templates and nocache ~= false then
566            cache[data] = known
567        end
568    elseif variables then
569        known.variables = variables
570    end
571    return known, known.variables
572end
573
574local function lmxresult(self,variables)
575    if self then
576        local converter = self.converter
577        if converter then
578            local converted = converter(variables)
579            if trace_variables then -- will become templates
580                report_lmx("converted size: %s",#converted)
581            end
582            return converted or lmxerror("no result from converter")
583        else
584            return lmxerror("invalid converter")
585        end
586    else
587        return lmxerror("invalid specification")
588    end
589end
590
591lmx.new    = lmxnew
592lmx.result = lmxresult
593
594local loadedfiles = { }
595
596function lmx.convertstring(templatestring,variables,nocache,path)
597    return lmxresult(lmxnew(templatestring,nil,nocache,path),variables)
598end
599
600function lmx.convertfile(templatefile,variables,nocache)
601    if trace_variables then -- will become templates
602        report_lmx("converting file %a",templatefile)
603    end
604    local converter = loadedfiles[templatefile]
605    if not converter then
606        converter = lmxnew(loadedfile(templatefile),nil,nocache,pathpart(templatefile))
607        loadedfiles[templatefile] = converter
608    end
609    return lmxresult(converter,variables)
610end
611
612local function lmxconvert(templatefile,resultfile,variables,nocache) -- or (templatefile,variables)
613    if trace_variables then -- will become templates
614        report_lmx("converting file %a",templatefile)
615    end
616    if not variables and type(resultfile) == "table" then
617        variables = resultfile
618    end
619    local converter = loadedfiles[templatefile]
620    if not converter then
621        converter = lmxnew(loadedfile(templatefile),nil,nocache,pathpart(templatefile))
622        if cache_files then
623            loadedfiles[templatefile] = converter
624        end
625    end
626    local result = lmxresult(converter,variables)
627    if resultfile then
628        io.savedata(resultfile,result)
629    else
630        return result
631    end
632end
633
634lmx.convert = lmxconvert
635
636-- helpers
637
638local nocomment    = (beginembedcss * (1 - endembedcss)^1 * endembedcss) / ""
639local nowhitespace = whitespace^1 / " " -- ""
640local semistripped = whitespace^1 / "" * P(";")
641local stripper     = Cs((nocomment + semistripped + nowhitespace + 1)^1)
642
643function lmx.stripcss(str)
644    return lpegmatch(stripper,str)
645end
646
647function lmx.color(r,g,b,a)
648    if r > 1 then
649        r = 1
650    end
651    if g > 1 then
652        g = 1
653    end
654    if b > 1 then
655        b = 1
656    end
657    if not a then
658        a= 0
659    elseif a > 1 then
660        a = 1
661    end
662    if a > 0 then
663        return format("rgba(%s%%,%s%%,%s%%,%s)",r*100,g*100,b*100,a)
664    else
665        return format("rgb(%s%%,%s%%,%s%%)",r*100,g*100,b*100)
666    end
667end
668
669-- these can be overloaded
670
671lmx.lmxfile   = string.itself
672lmx.htmfile   = string.itself
673lmx.popupfile = os.launch
674
675local function lmxmake(name,variables)
676    local lmxfile = lmx.lmxfile(name)
677    local htmfile = lmx.htmfile(name)
678    if lmxfile == htmfile then
679        htmfile = replacesuffix(lmxfile,"html")
680    end
681    lmxconvert(lmxfile,htmfile,variables)
682    return htmfile
683end
684
685lmx.make = lmxmake
686
687function lmx.show(name,variables)
688    -- todo: pcall
689    local htmfile = lmxmake(name,variables)
690    -- lmx.popupfile(htmfile)
691    return htmfile
692end
693
694-- Command line (will become mtx-lmx):
695
696if arg then
697    if     arg[1] == "--show"    then if arg[2] then lmx.show   (arg[2])                        end
698    elseif arg[1] == "--convert" then if arg[2] then lmx.convert(arg[2], arg[3] or "temp.html") end
699    end
700else
701    return lmx
702end
703
704-- Test 1:
705
706-- inspect(lmx.result(lmx.new(io.loaddata("t:/sources/context-timing.lmx"))))
707
708-- Test 2:
709
710-- local str = [[
711--     <?lmx-include context.css strip ?>
712--     <test>
713--         <?lmx-define-begin whatever?>some content a<?lmx-define-end ?>
714--         <?lmx-define-begin somemore?>some content b<?lmx-define-end ?>
715--         <more>
716--             <?lmx-resolve whatever ?>
717--             <?lua
718--                 for i=1,10 do end
719--             ?>
720--             <?lmx-resolve somemore ?>
721--         </more>
722--         <td><?lua p(100) ?></td>
723--         <td><?lua p(variables.a) ?></td>
724--         <td><?lua p(variables.b) ?></td>
725--         <td><?lua p(variables.c) ?></td>
726--         <td><?lua pv('title-default') ?></td>
727--     </test>
728-- ]]
729
730-- local defaults = { trace = true, a = 3, b = 3 }
731-- local result = lmx.new(str,defaults)
732-- inspect(result.data)
733-- inspect(result.converter(defaults))
734-- inspect(result.converter { a = 1 })
735-- inspect(lmx.result(result, { b = 2 }))
736-- inspect(lmx.result(result, { a = 20000, b = 40000 }))
737