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