xref: /freebsd/tools/lua/template.lua (revision 61ba55bcf70f2340f9c943c9571113b3fd8eda69)
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
32local setmetatable = setmetatable
33local loadstring = loadstring
34local tostring = tostring
35local setfenv = setfenv
36local require = require
37local concat = table.concat
38local assert = assert
39local write = io.write
40local pcall = pcall
41local phase
42local open = io.open
43local load = load
44local type = type
45local dump = string.dump
46local find = string.find
47local gsub = string.gsub
48local byte = string.byte
49local null
50local sub = string.sub
51local var
52
53local _VERSION = _VERSION
54local _ENV = _ENV -- luacheck: globals _ENV
55local _G = _G
56
57local HTML_ENTITIES = {
58    ["&"] = "&",
59    ["<"] = "&lt;",
60    [">"] = "&gt;",
61    ['"'] = "&quot;",
62    ["'"] = "&#39;",
63    ["/"] = "&#47;"
64}
65
66local CODE_ENTITIES = {
67    ["{"] = "&#123;",
68    ["}"] = "&#125;",
69    ["&"] = "&amp;",
70    ["<"] = "&lt;",
71    [">"] = "&gt;",
72    ['"'] = "&quot;",
73    ["'"] = "&#39;",
74    ["/"] = "&#47;"
75}
76
77local VAR_PHASES
78
79local ESC    = byte("\27")
80local NUL    = byte("\0")
81local HT     = byte("\t")
82local VT     = byte("\v")
83local LF     = byte("\n")
84local SOL    = byte("/")
85local BSOL   = byte("\\")
86local SP     = byte(" ")
87local AST    = byte("*")
88local NUM    = byte("#")
89local LPAR   = byte("(")
90local LSQB   = byte("[")
91local LCUB   = byte("{")
92local MINUS  = byte("-")
93local PERCNT = byte("%")
94
95local EMPTY  = ""
96
97local VIEW_ENV
98if _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 }
102else
103    VIEW_ENV = { __index = function(t, k)
104        return t.context[k] or t.template[k] or _ENV[k]
105    end }
106end
107
108local newtab
109do
110    local ok
111    ok, newtab = pcall(require, "table.new")
112    if not ok then newtab = function() return {} end end
113end
114
115local function enabled(val)
116    if val == nil then return true end
117    return val == true or (val == "1" or val == "true" or val == "on")
118end
119
120local function trim(s)
121    return gsub(gsub(s, "^%s+", EMPTY), "%s+$", EMPTY)
122end
123
124local 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
134end
135
136local 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
145end
146
147local 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
154end
155
156local 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
167end
168
169local function load_file(func)
170    return function(view) return func(view, false) end
171end
172
173local function load_string(func)
174    return function(view) return func(view, true) end
175end
176
177local function loader(template)
178    return function(view)
179	return assert(load(view, nil, nil, setmetatable({ template = template }, VIEW_ENV)))
180    end
181end
182
183local 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
193end
194
195local 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 = {[[
373context=... or {}
374local ___,blocks,layout={},blocks or {}
375local function include(v, c) return template.process(v, c or context) end
376local 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
649end
650
651return new()
652