xref: /freebsd/tools/lua/template.lua (revision 6ef644f5889afbd0f681b08ed1a2f369524af83e)
1 -- From lua-resty-template (modified to remove external dependencies)
2 --[[
3 Copyright (c) 2014 - 2020 Aapo Talvensaari
4 All rights reserved.
5 
6 Redistribution and use in source and binary forms, with or without modification,
7 are permitted provided that the following conditions are met:
8 
9 * Redistributions of source code must retain the above copyright notice, this
10   list of conditions and the following disclaimer.
11 
12 * Redistributions in binary form must reproduce the above copyright notice, this
13   list of conditions and the following disclaimer in the documentation and/or
14   other materials provided with the distribution.
15 
16 * Neither the name of the {organization} nor the names of its
17   contributors may be used to endorse or promote products derived from
18   this software without specific prior written permission.
19 
20 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
24 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 ]]--
31 
32 local setmetatable = setmetatable
33 local loadstring = loadstring
34 local tostring = tostring
35 local setfenv = setfenv
36 local require = require
37 local concat = table.concat
38 local assert = assert
39 local write = io.write
40 local pcall = pcall
41 local phase
42 local open = io.open
43 local load = load
44 local type = type
45 local dump = string.dump
46 local find = string.find
47 local gsub = string.gsub
48 local byte = string.byte
49 local null
50 local sub = string.sub
51 local var
52 
53 local _VERSION = _VERSION
54 local _ENV = _ENV -- luacheck: globals _ENV
55 local _G = _G
56 
57 local HTML_ENTITIES = {
58     ["&"] = "&",
59     ["<"] = "&lt;",
60     [">"] = "&gt;",
61     ['"'] = "&quot;",
62     ["'"] = "&#39;",
63     ["/"] = "&#47;"
64 }
65 
66 local CODE_ENTITIES = {
67     ["{"] = "&#123;",
68     ["}"] = "&#125;",
69     ["&"] = "&amp;",
70     ["<"] = "&lt;",
71     [">"] = "&gt;",
72     ['"'] = "&quot;",
73     ["'"] = "&#39;",
74     ["/"] = "&#47;"
75 }
76 
77 local VAR_PHASES
78 
79 local ESC    = byte("\27")
80 local NUL    = byte("\0")
81 local HT     = byte("\t")
82 local VT     = byte("\v")
83 local LF     = byte("\n")
84 local SOL    = byte("/")
85 local BSOL   = byte("\\")
86 local SP     = byte(" ")
87 local AST    = byte("*")
88 local NUM    = byte("#")
89 local LPAR   = byte("(")
90 local LSQB   = byte("[")
91 local LCUB   = byte("{")
92 local MINUS  = byte("-")
93 local PERCNT = byte("%")
94 
95 local EMPTY  = ""
96 
97 local VIEW_ENV
98 if _VERSION == "Lua 5.1" then
99     VIEW_ENV = { __index = function(t, k)
100         return t.context[k] or t.template[k] or _G[k]
101     end }
102 else
103     VIEW_ENV = { __index = function(t, k)
104         return t.context[k] or t.template[k] or _ENV[k]
105     end }
106 end
107 
108 local newtab
109 do
110     local ok
111     ok, newtab = pcall(require, "table.new")
112     if not ok then newtab = function() return {} end end
113 end
114 
115 local function enabled(val)
116     if val == nil then return true end
117     return val == true or (val == "1" or val == "true" or val == "on")
118 end
119 
120 local function trim(s)
121     return gsub(gsub(s, "^%s+", EMPTY), "%s+$", EMPTY)
122 end
123 
124 local function rpos(view, s)
125     while s > 0 do
126         local c = byte(view, s, s)
127         if c == SP or c == HT or c == VT or c == NUL then
128             s = s - 1
129         else
130             break
131         end
132     end
133     return s
134 end
135 
136 local function escaped(view, s)
137     if s > 1 and byte(view, s - 1, s - 1) == BSOL then
138         if s > 2 and byte(view, s - 2, s - 2) == BSOL then
139             return false, 1
140         else
141             return true, 1
142         end
143     end
144     return false, 0
145 end
146 
147 local function read_file(path)
148     local file, err = open(path, "rb")
149     if not file then return nil, err end
150     local content
151     content, err = file:read "*a"
152     file:close()
153     return content, err
154 end
155 
156 local function load_view(template)
157     return function(view, plain)
158 	if plain == true then return view end
159 	local path, root = view, template.root
160 	if root and root ~= EMPTY then
161 	    if byte(root, -1) == SOL then root = sub(root, 1, -2) end
162 	    if byte(view,  1) == SOL then path = sub(view, 2) end
163 	    path = root .. "/" .. path
164 	end
165 	return plain == false and assert(read_file(path)) or read_file(path) or view
166     end
167 end
168 
169 local function load_file(func)
170     return function(view) return func(view, false) end
171 end
172 
173 local function load_string(func)
174     return function(view) return func(view, true) end
175 end
176 
177 local function loader(template)
178     return function(view)
179 	return assert(load(view, nil, nil, setmetatable({ template = template }, VIEW_ENV)))
180     end
181 end
182 
183 local function visit(visitors, content, tag, name)
184     if not visitors then
185         return content
186     end
187 
188     for i = 1, visitors.n do
189         content = visitors[i](content, tag, name)
190     end
191 
192     return content
193 end
194 
195 local function new(template, safe)
196     template = template or newtab(0, 26)
197 
198     template._VERSION    = "2.0"
199     template.cache       = {}
200     template.load        = load_view(template)
201     template.load_file   = load_file(template.load)
202     template.load_string = load_string(template.load)
203     template.print       = write
204 
205     local load_chunk = loader(template)
206 
207     local caching
208     if VAR_PHASES and VAR_PHASES[phase()] then
209         caching = enabled(var.template_cache)
210     else
211         caching = true
212     end
213 
214     local visitors
215     function template.visit(func)
216         if not visitors then
217             visitors = { func, n = 1 }
218             return
219         end
220         visitors.n = visitors.n + 1
221         visitors[visitors.n] = func
222     end
223 
224     function template.caching(enable)
225         if enable ~= nil then caching = enable == true end
226         return caching
227     end
228 
229     function template.output(s)
230         if s == nil or s == null then return EMPTY end
231         if type(s) == "function" then return template.output(s()) end
232         return tostring(s)
233     end
234 
235     function template.escape(s, c)
236         if type(s) == "string" then
237             if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end
238             return gsub(s, "[\">/<'&]", HTML_ENTITIES)
239         end
240         return template.output(s)
241     end
242 
243     function template.new(view, layout)
244         local vt = type(view)
245 
246         if vt == "boolean" then return new(nil,  view) end
247         if vt == "table"   then return new(view, safe) end
248         if vt == "nil"     then return new(nil,  safe) end
249 
250         local render
251         local process
252         if layout then
253             if type(layout) == "table" then
254                 render = function(self, context)
255                     context = context or self
256                     context.blocks = context.blocks or {}
257                     context.view = template.process(view, context)
258                     layout.blocks = context.blocks or {}
259                     layout.view = context.view or EMPTY
260                     layout:render()
261                 end
262                 process = function(self, context)
263                     context = context or self
264                     context.blocks = context.blocks or {}
265                     context.view = template.process(view, context)
266                     layout.blocks = context.blocks or {}
267                     layout.view = context.view
268                     return tostring(layout)
269                 end
270             else
271                 render = function(self, context)
272                     context = context or self
273                     context.blocks = context.blocks or {}
274                     context.view = template.process(view, context)
275                     template.render(layout, context)
276                 end
277                 process = function(self, context)
278                     context = context or self
279                     context.blocks = context.blocks or {}
280                     context.view = template.process(view, context)
281                     return template.process(layout, context)
282                 end
283             end
284         else
285             render = function(self, context)
286                 return template.render(view, context or self)
287             end
288             process = function(self, context)
289                 return template.process(view, context or self)
290             end
291         end
292 
293         if safe then
294             return setmetatable({
295                 render = function(...)
296                     local ok, err = pcall(render, ...)
297                     if not ok then
298                         return nil, err
299                     end
300                 end,
301                 process = function(...)
302                     local ok, output = pcall(process, ...)
303                     if not ok then
304                         return nil, output
305                     end
306                     return output
307                 end,
308              }, {
309                 __tostring = function(...)
310                     local ok, output = pcall(process, ...)
311                     if not ok then
312                         return ""
313                     end
314                     return output
315             end })
316         end
317 
318         return setmetatable({
319             render = render,
320             process = process
321         }, {
322             __tostring = process
323         })
324     end
325 
326     function template.precompile(view, path, strip, plain)
327         local chunk = dump(template.compile(view, nil, plain), strip ~= false)
328         if path then
329             local file = open(path, "wb")
330             file:write(chunk)
331             file:close()
332         end
333         return chunk
334     end
335 
336     function template.precompile_string(view, path, strip)
337         return template.precompile(view, path, strip, true)
338     end
339 
340     function template.precompile_file(view, path, strip)
341         return template.precompile(view, path, strip, false)
342     end
343 
344     function template.compile(view, cache_key, plain)
345         assert(view, "view was not provided for template.compile(view, cache_key, plain)")
346         if cache_key == "no-cache" then
347             return load_chunk(template.parse(view, plain)), false
348         end
349         cache_key = cache_key or view
350         local cache = template.cache
351         if cache[cache_key] then return cache[cache_key], true end
352         local func = load_chunk(template.parse(view, plain))
353         if caching then cache[cache_key] = func end
354         return func, false
355     end
356 
357     function template.compile_file(view, cache_key)
358         return template.compile(view, cache_key, false)
359     end
360 
361     function template.compile_string(view, cache_key)
362         return template.compile(view, cache_key, true)
363     end
364 
365     function template.parse(view, plain)
366         assert(view, "view was not provided for template.parse(view, plain)")
367         if plain ~= true then
368             view = template.load(view, plain)
369             if byte(view, 1, 1) == ESC then return view end
370         end
371         local j = 2
372         local c = {[[
373 context=... or {}
374 local ___,blocks,layout={},blocks or {}
375 local function include(v, c) return template.process(v, c or context) end
376 local function echo(...) for i=1,select("#", ...) do ___[#___+1] = tostring(select(i, ...)) end end
377 ]] }
378         local i, s = 1, find(view, "{", 1, true)
379         while s do
380             local t, p = byte(view, s + 1, s + 1), s + 2
381             if t == LCUB then
382                 local e = find(view, "}}", p, true)
383                 if e then
384                     local z, w = escaped(view, s)
385                     if i < s - w then
386                         c[j] = "___[#___+1]=[=[\n"
387                         c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
388                         c[j+2] = "]=]\n"
389                         j=j+3
390                     end
391                     if z then
392                         i = s
393                     else
394                         c[j] = "___[#___+1]=template.escape("
395                         c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "{")
396                         c[j+2] = ")\n"
397                         j=j+3
398                         s, i = e + 1, e + 2
399                     end
400                 end
401             elseif t == AST then
402                 local e = find(view, "*}", p, true)
403                 if e then
404                     local z, w = escaped(view, s)
405                     if i < s - w then
406                         c[j] = "___[#___+1]=[=[\n"
407                         c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
408                         c[j+2] = "]=]\n"
409                         j=j+3
410                     end
411                     if z then
412                         i = s
413                     else
414                         c[j] = "___[#___+1]=template.output("
415                         c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "*")
416                         c[j+2] = ")\n"
417                         j=j+3
418                         s, i = e + 1, e + 2
419                     end
420                 end
421             elseif t == PERCNT then
422                 local e = find(view, "%}", p, true)
423                 if e then
424                     local z, w = escaped(view, s)
425                     if z then
426                         if i < s - w then
427                             c[j] = "___[#___+1]=[=[\n"
428                             c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
429                             c[j+2] = "]=]\n"
430                             j=j+3
431                         end
432                         i = s
433                     else
434                         local n = e + 2
435                         if byte(view, n, n) == LF then
436                             n = n + 1
437                         end
438                         local r = rpos(view, s - 1)
439                         if i <= r then
440                             c[j] = "___[#___+1]=[=[\n"
441                             c[j+1] = visit(visitors, sub(view, i, r))
442                             c[j+2] = "]=]\n"
443                             j=j+3
444                         end
445                         c[j] = visit(visitors, trim(sub(view, p, e - 1)), "%")
446                         c[j+1] = "\n"
447                         j=j+2
448                         s, i = n - 1, n
449                     end
450                 end
451             elseif t == LPAR then
452                 local e = find(view, ")}", p, true)
453                 if e then
454                     local z, w = escaped(view, s)
455                     if i < s - w then
456                         c[j] = "___[#___+1]=[=[\n"
457                         c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
458                         c[j+2] = "]=]\n"
459                         j=j+3
460                     end
461                     if z then
462                         i = s
463                     else
464                         local f = visit(visitors, sub(view, p, e - 1), "(")
465                         local x = find(f, ",", 2, true)
466                         if x then
467                             c[j] = "___[#___+1]=include([=["
468                             c[j+1] = trim(sub(f, 1, x - 1))
469                             c[j+2] = "]=],"
470                             c[j+3] = trim(sub(f, x + 1))
471                             c[j+4] = ")\n"
472                             j=j+5
473                         else
474                             c[j] = "___[#___+1]=include([=["
475                             c[j+1] = trim(f)
476                             c[j+2] = "]=])\n"
477                             j=j+3
478                         end
479                         s, i = e + 1, e + 2
480                     end
481                 end
482             elseif t == LSQB then
483                 local e = find(view, "]}", p, true)
484                 if e then
485                     local z, w = escaped(view, s)
486                     if i < s - w then
487                         c[j] = "___[#___+1]=[=[\n"
488                         c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
489                         c[j+2] = "]=]\n"
490                         j=j+3
491                     end
492                     if z then
493                         i = s
494                     else
495                         c[j] = "___[#___+1]=include("
496                         c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "[")
497                         c[j+2] = ")\n"
498                         j=j+3
499                         s, i = e + 1, e + 2
500                     end
501                 end
502             elseif t == MINUS then
503                 local e = find(view, "-}", p, true)
504                 if e then
505                     local x, y = find(view, sub(view, s, e + 1), e + 2, true)
506                     if x then
507                         local z, w = escaped(view, s)
508                         if z then
509                             if i < s - w then
510                                 c[j] = "___[#___+1]=[=[\n"
511                                 c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
512                                 c[j+2] = "]=]\n"
513                                 j=j+3
514                             end
515                             i = s
516                         else
517                             y = y + 1
518                             x = x - 1
519                             if byte(view, y, y) == LF then
520                                 y = y + 1
521                             end
522                             local b = trim(sub(view, p, e - 1))
523                             if b == "verbatim" or b == "raw" then
524                                 if i < s - w then
525                                     c[j] = "___[#___+1]=[=[\n"
526                                     c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
527                                     c[j+2] = "]=]\n"
528                                     j=j+3
529                                 end
530                                 c[j] = "___[#___+1]=[=["
531                                 c[j+1] = visit(visitors, sub(view, e + 2, x))
532                                 c[j+2] = "]=]\n"
533                                 j=j+3
534                             else
535                                 if byte(view, x, x) == LF then
536                                     x = x - 1
537                                 end
538                                 local r = rpos(view, s - 1)
539                                 if i <= r then
540                                     c[j] = "___[#___+1]=[=[\n"
541                                     c[j+1] = visit(visitors, sub(view, i, r))
542                                     c[j+2] = "]=]\n"
543                                     j=j+3
544                                 end
545                                 c[j] = 'blocks["'
546                                 c[j+1] = b
547                                 c[j+2] = '"]=include[=['
548                                 c[j+3] = visit(visitors, sub(view, e + 2, x), "-", b)
549                                 c[j+4] = "]=]\n"
550                                 j=j+5
551                             end
552                             s, i = y - 1, y
553                         end
554                     end
555                 end
556             elseif t == NUM then
557                 local e = find(view, "#}", p, true)
558                 if e then
559                     local z, w = escaped(view, s)
560                     if i < s - w then
561                         c[j] = "___[#___+1]=[=[\n"
562                         c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
563                         c[j+2] = "]=]\n"
564                         j=j+3
565                     end
566                     if z then
567                         i = s
568                     else
569                         e = e + 2
570                         if byte(view, e, e) == LF then
571                             e = e + 1
572                         end
573                         s, i = e - 1, e
574                     end
575                 end
576             end
577             s = find(view, "{", s + 1, true)
578         end
579         s = sub(view, i)
580         if s and s ~= EMPTY then
581             c[j] = "___[#___+1]=[=[\n"
582             c[j+1] = visit(visitors, s)
583             c[j+2] = "]=]\n"
584             j=j+3
585         end
586         c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)" -- luacheck: ignore
587         return concat(c)
588     end
589 
590     function template.parse_file(view)
591         return template.parse(view, false)
592     end
593 
594     function template.parse_string(view)
595         return template.parse(view, true)
596     end
597 
598     function template.process(view, context, cache_key, plain)
599         assert(view, "view was not provided for template.process(view, context, cache_key, plain)")
600         return template.compile(view, cache_key, plain)(context)
601     end
602 
603     function template.process_file(view, context, cache_key)
604         assert(view, "view was not provided for template.process_file(view, context, cache_key)")
605         return template.compile(view, cache_key, false)(context)
606     end
607 
608     function template.process_string(view, context, cache_key)
609         assert(view, "view was not provided for template.process_string(view, context, cache_key)")
610         return template.compile(view, cache_key, true)(context)
611     end
612 
613     function template.render(view, context, cache_key, plain)
614         assert(view, "view was not provided for template.render(view, context, cache_key, plain)")
615         template.print(template.process(view, context, cache_key, plain))
616     end
617 
618     function template.render_file(view, context, cache_key)
619         assert(view, "view was not provided for template.render_file(view, context, cache_key)")
620         template.render(view, context, cache_key, false)
621     end
622 
623     function template.render_string(view, context, cache_key)
624         assert(view, "view was not provided for template.render_string(view, context, cache_key)")
625         template.render(view, context, cache_key, true)
626     end
627 
628     if safe then
629         return setmetatable({}, {
630             __index = function(_, k)
631                 if type(template[k]) == "function" then
632                     return function(...)
633                         local ok, a, b = pcall(template[k], ...)
634                         if not ok then
635                             return nil, a
636                         end
637                         return a, b
638                     end
639                 end
640                 return template[k]
641             end,
642             __new_index = function(_, k, v)
643                 template[k] = v
644             end,
645         })
646     end
647 
648     return template
649 end
650 
651 return new()
652