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