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