util-tpl.lua /size: 7722 b    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['util-tpl'] = {
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-- This is experimental code. Coming from dos and windows, I've always used %whatever%
10-- as template variables so let's stick to it. After all, it's easy to parse and stands
11-- out well. A double %% is turned into a regular %.
12
13utilities.templates = utilities.templates or { }
14local templates     = utilities.templates
15
16local trace_template  = false  trackers.register("templates.trace",function(v) trace_template = v end)
17local report_template = logs.reporter("template")
18
19local tostring, next = tostring, next
20local format, sub, byte = string.format, string.sub, string.byte
21local P, C, R, Cs, Cc, Carg, lpegmatch, lpegpatterns = lpeg.P, lpeg.C, lpeg.R, lpeg.Cs, lpeg.Cc, lpeg.Carg, lpeg.match, lpeg.patterns
22
23local formatters = string.formatters
24
25-- todo: make installable template.new
26
27local replacer
28
29local function replacekey(k,t,how,recursive)
30    local v = t[k]
31    if not v then
32        if trace_template then
33            report_template("unknown key %a",k)
34        end
35        return ""
36    else
37        v = tostring(v)
38        if trace_template then
39            report_template("setting key %a to value %a",k,v)
40        end
41        if recursive then
42            return lpegmatch(replacer,v,1,t,how,recursive)
43        else
44            return v
45        end
46    end
47end
48
49local sqlescape = lpeg.replacer {
50    { "'",    "''"   },
51    { "\\",   "\\\\" },
52    { "\r\n", "\\n"  },
53    { "\r",   "\\n"  },
54 -- { "\t",   "\\t"  },
55}
56
57local sqlquoted = Cs(Cc("'") * sqlescape * Cc("'"))
58
59lpegpatterns.sqlescape = sqlescape
60lpegpatterns.sqlquoted = sqlquoted
61
62-- escapeset  : \0\1\2\3\4\5\6\7\8\9\10\11\12\13\14\15\16\17\18\19\20\21\22\23\24\25\26\27\28\29\30\31\"\\\127
63-- test string: [[1\0\31test23"\\]] .. string.char(19) .. "23"
64--
65-- slow:
66--
67-- local luaescape = lpeg.replacer {
68--     { '"',  [[\"]]   },
69--     { '\\', [[\\]]   },
70--     { R("\0\9")   * #R("09"), function(s) return "\\00" .. byte(s) end },
71--     { R("\10\31") * #R("09"), function(s) return "\\0"  .. byte(s) end },
72--     { R("\0\31")            , function(s) return "\\"   .. byte(s) end },
73--     }
74--
75-- slightly faster:
76
77-- local luaescape = Cs ((
78--     P('"' ) / [[\"]] +
79--     P('\\') / [[\\]] +
80--     Cc("\\00") * (R("\0\9")   / byte) * #R("09") +
81--     Cc("\\0")  * (R("\10\31") / byte) * #R("09") +
82--     Cc("\\")   * (R("\0\31")  / byte)                 +
83--     P(1)
84-- )^0)
85
86----- xmlescape = lpegpatterns.xmlescape
87----- texescape = lpegpatterns.texescape
88local luaescape = lpegpatterns.luaescape
89----- sqlquoted = lpegpatterns.sqlquoted
90----- luaquoted = lpegpatterns.luaquoted
91
92local escapers = {
93    lua = function(s)
94     -- return sub(format("%q",s),2,-2)
95        return lpegmatch(luaescape,s)
96    end,
97    sql = function(s)
98        return lpegmatch(sqlescape,s)
99    end,
100}
101
102local quotedescapers = {
103    lua = function(s)
104     -- return lpegmatch(luaquoted,s)
105        return format("%q",s)
106    end,
107    sql = function(s)
108        return lpegmatch(sqlquoted,s)
109    end,
110}
111
112local luaescaper       = escapers.lua
113local quotedluaescaper = quotedescapers.lua
114
115local function replacekeyunquoted(s,t,how,recurse) -- ".. \" "
116    if how == false then
117        return replacekey(s,t,how,recurse)
118    else
119        local escaper = how and escapers[how] or luaescaper
120        return escaper(replacekey(s,t,how,recurse))
121    end
122end
123
124local function replacekeyquoted(s,t,how,recurse) -- ".. \" "
125    if how == false then
126        return replacekey(s,t,how,recurse)
127    else
128        local escaper = how and quotedescapers[how] or quotedluaescaper
129        return escaper(replacekey(s,t,how,recurse))
130    end
131end
132
133local function replaceoptional(l,m,r,t,how,recurse)
134    local v = t[l]
135    return v and v ~= "" and lpegmatch(replacer,r,1,t,how or "lua",recurse or false) or ""
136end
137
138local function replaceformatted(l,m,r,t,how,recurse)
139    local v = t[r]
140    return v and formatters[l](v)
141end
142
143local single       = P("%")  -- test %test% test     : resolves test
144local double       = P("%%") -- test 10%% test       : %% becomes %
145local lquoted      = P("%[") -- test '%[test]%' test : resolves to test with escaped "'s
146local rquoted      = P("]%") --
147local lquotedq     = P("%(") -- test %(test)% test   : resolves to 'test' with escaped "'s
148local rquotedq     = P(")%") --
149
150local escape       = double   / '%%'
151local nosingle     = single   / ''
152local nodouble     = double   / ''
153local nolquoted    = lquoted  / ''
154local norquoted    = rquoted  / ''
155local nolquotedq   = lquotedq / ''
156local norquotedq   = rquotedq / ''
157
158local nolformatted = P(":") / "%%"
159local norformatted = P(":") / ""
160
161local noloptional  = P("%?") / ''
162local noroptional  = P("?%") / ''
163local nomoptional  = P(":")  / ''
164
165local args         = Carg(1) * Carg(2) * Carg(3)
166local key          = nosingle * ((C((1-nosingle)^1) * args) / replacekey) * nosingle
167local quoted       = nolquotedq * ((C((1-norquotedq)^1) * args) / replacekeyquoted) * norquotedq
168local unquoted     = nolquoted * ((C((1-norquoted)^1) * args) / replacekeyunquoted) * norquoted
169local optional     = noloptional * ((C((1-nomoptional)^1) * nomoptional * C((1-noroptional)^1) * args) / replaceoptional) *  noroptional
170local formatted    = nosingle * ((Cs(nolformatted * (1-norformatted )^1) * norformatted * C((1-nosingle)^1) * args) / replaceformatted) * nosingle
171local any          = P(1)
172
173      replacer     = Cs((unquoted + quoted + formatted + escape + optional + key + any)^0)
174
175local function replace(str,mapping,how,recurse)
176    if mapping and str then
177        return lpegmatch(replacer,str,1,mapping,how or "lua",recurse or false) or str
178    else
179        return str
180    end
181end
182
183-- print(replace("test '%[x]%' test",{ x = [[a 'x'  a]] }))
184-- print(replace("test '%x%' test",{ x = [[a "x"  a]] }))
185-- print(replace([[test "%[x]%" test]],{ x = [[a "x"  a]] }))
186-- print(replace("test '%[x]%' test",{ x = true }))
187-- print(replace("test '%[x]%' test",{ x = [[a 'x'  a]], y = "oeps" },'sql'))
188-- print(replace("test '%[x]%' test",{ x = [[a '%y%'  a]], y = "oeps" },'sql',true))
189-- print(replace([[test %[x]% test]],{ x = [[a "x"  a]]}))
190-- print(replace([[test %(x)% test]],{ x = [[a "x"  a]]}))
191-- print(replace([[convert %?x: -x "%x%" ?% %?y: -y "%y%" ?%]],{ x = "yes" }))
192-- print(replace("test %:0.3N:x% test",{ x = 123.45 }))
193-- print(replace("test %:0.3N:x% test",{ x = 12345 }))
194
195templates.replace = replace
196
197function templates.replacer(str,how,recurse) -- reads nicer
198    return function(mapping)
199        return lpegmatch(replacer,str,1,mapping,how or "lua",recurse or false) or str
200    end
201end
202
203-- local cmd = templates.replacer([[foo %bar%]]) print(cmd { bar = "foo" })
204
205function templates.load(filename,mapping,how,recurse)
206    local data = io.loaddata(filename) or ""
207    if mapping and next(mapping) then
208        return replace(data,mapping,how,recurse)
209    else
210        return data
211    end
212end
213
214function templates.resolve(t,mapping,how,recurse)
215    if not mapping then
216        mapping = t
217    end
218    for k, v in next, t do
219        t[k] = replace(v,mapping,how,recurse)
220    end
221    return t
222end
223
224-- inspect(utilities.templates.replace("test %one% test", { one = "%two%", two = "two" }))
225-- inspect(utilities.templates.resolve({ one = "%two%", two = "two", three = "%three%" }))
226-- inspect(utilities.templates.replace("test %one% test", { one = "%two%", two = "two" },false,true))
227-- inspect(utilities.templates.replace("test %one% test", { one = "%two%", two = "two" },false))
228