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 ["<"] = "<", 61 [">"] = ">", 62 ['"'] = """, 63 ["'"] = "'", 64 ["/"] = "/" 65} 66 67local CODE_ENTITIES = { 68 ["{"] = "{", 69 ["}"] = "}", 70 ["&"] = "&", 71 ["<"] = "<", 72 [">"] = ">", 73 ['"'] = """, 74 ["'"] = "'", 75 ["/"] = "/" 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