sokol-luajit/editor/twig/aspect/template.lua

480 lines
15 KiB
Lua

local setmetatable = setmetatable
local compiler = require("aspect.compiler")
local output = require("aspect.output")
local funcs = require("aspect.funcs")
local filters = require("aspect.filters")
local tests = require("aspect.tests")
local err = require("aspect.error")
local utils = require("aspect.utils")
local config = require("aspect.config")
local var_dump = utils.var_dump
local loadcode
local loadchunk
local _VERSION = _VERSION
local loadstring = loadstring
local load = load
local setfenv = setfenv
local function_dump = string.dump
local tostring = tostring
local pairs = pairs
local ipairs = ipairs
local type = type
local pcall = pcall
local next = next
local jit = jit
--- View. Object representation of template.
--- @class aspect.view
--- @field v number view version
--- @field name string the template name
--- @field body function body function
--- @field macros table hashtable of the macros
--- @field blocks table<aspect.template.block> hashtable of the blocks
--- @field uses table<aspect.template.use> hashtable of {% use %} tags
--- @field extends string|boolean parent template if parent's name is string, or just true if name unknown (dynamically)
--- @class aspect.template.block
--- @field body fun(__: aspect.output, context: table) block's code
--- @field vars table used context variables
--- @field parent boolean use parent() function inside
--- @class aspect.template.use
--- @field name string the external template name
--- @field with table blocks map like { remote_name = "local_name", ... }
--- @class aspect.template
--- @field compiler aspect.compiler
--- @field stats boolean
--- @field cache boolean|table enable or disable in-memory cache. by default: false. set table (as container) for enable cache.
--- @field loader fun(name: string):string,string template source code loader with etag (optionally)
--- @field luacode_load fun(name: string, tpl: aspect.template):string
--- @field luacode_save fun(name: string, luacode: string, tpl: aspect.template)
--- @field bytecode_load fun(name: string, tpl: aspect.template):string
--- @field bytecode_save fun(name: string, bytecode: string, tpl: aspect.template)
local template = {
_VERSION = "2.2",
_NAME = "aspect",
}
local mt = { __index = template }
do
if _VERSION == "Lua 5.1" then
if jit then
loadchunk = function(tpl, code, name)
return load(code, name, nil, tpl.env)
end
else
loadchunk = function(tpl, code, name)
local func, error = loadstring(code, name)
if func then
setfenv(func, tpl.env)
end
return func, error
end
end
else
loadchunk = function(tpl, code, name)
return load(code, name, nil, tpl.env)
end
end
--- @param tpl aspect.template
--- @param code string
--- @param name string the template name
loadcode = function (tpl, code, name)
local error
if type(code) == 'string' then
code, error = loadchunk(tpl, code, name .. ".lua")
if error then
return nil, err.new("Error loading '" .. name .. "' code: " .. tostring(error))
end
elseif type(code) ~= 'function' then
return nil, err.new("loaded view '" .. name .. "' is not valid a function or bytecode")
end
return code(), nil, code
end
end
--- @param options table
function template.new(options)
options = options or {}
local tpl = setmetatable({
cache = false,
compiler = compiler,
loader = options.loader,
env = setmetatable(options.env or {}, {__index = config.env}),
luacode_load = options.luacode_load,
luacode_save = options.luacode_save,
bytecode_load = options.bytecode_load,
bytecode_save = options.bytecode_save,
}, mt)
if options.cache then
if options.cache == true then
tpl.cache = {}
else
tpl.cache = options.cache
end
end
tpl.opts = {
escape = options.autoescape or false,
strip = options.autostrip or false,
stack_size = 20,
f = filters.fn,
fn = funcs.fn,
t = tests.fn
}
tpl.stats = options.stats or true
--- @param names table|string
tpl.opts.get = function(names)
local view, error
if type(names) == "table" then
for _, n in ipairs(names) do
view, error = tpl:get_view(n)
if error then
return nil, error
elseif view then
return view
end
end
else
view, error = tpl:get_view(names)
if error then
return nil, error
elseif view then
return view
end
end
end
return tpl
end
--- Parse the template.
--- If `source` not nil then code will be parsed else source code will be loaded via loader by name.
--- @param name string the template name
--- @param source string|nil
--- @return aspect.compiler
--- @return aspect.error
function template:parse(name, source)
local error, ok
if not source then
source, error = self.loader(name, self)
if not source then
return nil, nil, err.new(error)
end
end
local cmp = self.compiler.new(self, name)
ok, error = cmp:run(source)
if not ok then
return nil, error
else
return cmp
end
end
--- Prepare the view (import blocks from {% use %} tags)
--- @param tpl aspect.template
--- @param view aspect.view
local function expand(tpl, view)
-- lazy load {% use %} tags
if view.uses then
for _, use in ipairs(view.uses) do
local use_view, use_error = tpl:get_view(use.name)
if use_view then -- use template loaded
if next(use_view) then -- loaded template has blocks
if use.with then -- we use peace of blocks
for n, a in pairs(use.with) do
if not view.blocks[a] and use_view.blocks[n] then
view.blocks[a] = use_view.blocks[n]
end
end
else
for n, b in pairs(use_view.blocks) do
if not view.blocks[n] then
view.blocks[n] = b
end
end
end
end
elseif use_error then
return err.new("Failed to load block from template " .. view.name .. ": "
.. err.new(use_error):get_message()):set_name(view.name, use_view and use_view.line or nil)
end
end
end
end
--- Parse, compile and prepare the template.
--- If `source` not nil then code will be compiled else source code will be loaded via loader by name.
--- If `aspect.error` and `aspect.compiler` is nil — the template not found
--- @param name string
--- @param source string
--- @param cache boolean save into cache
--- @return aspect.error if error occurred
--- @return aspect.compiler
--- @return aspect.view result view
--- @return function view as function for dumping to bytecode
function template:compile(name, source, cache)
local ok, error, code, func, view
if not source then
source, error = self.loader(name, self)
if not source then
if error then
return err.new(error, "compile"), nil, nil, nil
else
return nil
end
end
end
local build = self.compiler.new(self, name)
ok, error = build:run(source)
if not ok then
return error, build, nil, nil
end
code = build:get_code()
func, error = loadchunk(self, code, name .. ".lua")
if error then
return err.new("Error loading '" .. name .. "' code: " .. tostring(error) .. "\n" .. utils.numerate_lines(code)), build
end
view, error = loadcode(self, code, name)
if not view then
return error, build, nil, nil
end
error = expand(self, view)
if error then
return error, build, view, func
end
if self.cache and cache then
self.cache[view.name] = view
end
return nil, build, view, func
end
--- Returns the view
--- @param name string the view name
--- @param source string the source code
--- @return aspect.view|nil
--- @return aspect.error|nil
--- @return aspect.compiler|nil
function template:get_view(name)
if self.cache and self.cache[name] then
return self.cache[name]
end
return self:load(name)
end
--- Load template and compile template if needed.
--- This method works without internal template cache.
--- @param name string the view name
--- @return aspect.view|nil
--- @return aspect.error or nil if OK
--- @return aspect.compiler or nil of the template from cache
function template:load(name)
local bytecode, luacode, build, error, f, view
if self.bytecode_load then
bytecode, error = self.bytecode_load(name, self)
if bytecode then
view = loadcode(self, bytecode, name)
elseif error then
return nil, err.new(error)
end
end
if not view and self.luacode_load then
luacode, error = self.luacode_load(name, self)
if luacode then
view, error, f = loadcode(self, luacode, name)
if error then
return nil, error
end
if self.bytecode_save then
self.bytecode_save(name, function_dump(f), self)
end
elseif error then
return nil, err.new(error)
end
end
if view then
error = expand(self, view)
if error then
return nil, error
end
if self.cache then
self.cache[view.name] = view
end
return view
end
error, build, view, f = self:compile(name)
if error then
return nil, error, build
elseif not view then
return nil
end
if self.luacode_save then
self.luacode_save(name, build:get_code(), self)
end
if self.bytecode_save then
self.bytecode_save(name, function_dump(f), self)
end
return view, nil, build
end
--- @param name string
--- @param source string
--- @param vars table
--- @param options table|nil
--- @overload fun(source:string, vars:table, name:string)
--- @overload fun(source:string, vars:table)
function template:eval(source, vars, options, name)
local error, build, view = self:compile(name or "eval", source, name)
if error then
return nil, error, build
end
return self:render_view(view, vars, options)
end
--- @param view aspect.view
--- @param vars table
--- @param options table
function template:render_view(view, vars, options)
local out, ok, error = output.new(self.opts, vars, options or {}), nil, nil
out:push_view(view)
while view.extends do
local extends = view.extends
if extends == true then -- dynamic extends
ok, extends = pcall(view.body, out, vars)
if not ok then
if err.is(extends) then
return out, extends
else
return out, err.new({
code = "runtime",
name = view.name,
line = out.line,
message = tostring(extends)
})
end
end
end
local v, e = self:get_view(extends)
if not v then
return nil, err.new(e or "Template '" .. tostring(view.extends) .. "' not found while extending " .. view.name)
end
view = v
out:push_view(view)
end
ok, error = pcall(view.body, out, vars)
--print(ok,error)
if ok then
return out:finish()
elseif err.is(error) then
return out:finish(), error
else
return out:finish(), err.new({
code = "runtime",
name = view.name,
line = out.line,
message = tostring(error)
})
end
end
--- Returns template result as string
--- @param name string the template name
--- @param vars table
--- @return aspect.output template result
--- @return aspect.error if error occur
function template:render(name, vars, options)
local view, error = self:get_view(name)
if not view then
return nil, err.new(error or "Template '" .. tostring(name) .. "' not found")
end
return self:render_view(view, vars, options)
end
--- Returns macro result as string
--- @param name string the template name
--- @param macro_name string the macro name
--- @param arguments table of arguments for macro
--- @return aspect.output macro result
--- @return aspect.error if error occur
function template:render_macro(name, macro_name, arguments)
local out, ok = output.new(self.opts, arguments, {}), nil
local view, error = self:get_view(name)
if not view then
return nil, err.new(error or "Template '" .. tostring(name) .. "' not found")
end
if view.macros[macro_name] then
ok, error = pcall(view.macros[macro_name], out, arguments)
if ok then
return tostring(out)
else
return out:finish(), err.runtime_error(out, error)
end
else
return out:finish(), err.new({
name = view.name,
message = "Macro '" .. macro_name .. "' not found"
})
end
end
--- Returns block result as string
--- @param name string the template name
--- @param block_name string the block name
--- @param vars table of arguments for block
--- @return aspect.output block result
--- @return aspect.error if error occur
function template:render_block(name, block_name, vars)
local out, ok = output.new(self.opts, vars, {}), nil
local view, error = self:get_view(name)
if not view then
return nil, err.new(error or "Template '" .. tostring(name) .. "' not found")
end
if view.blocks and view.blocks[block_name] then
ok, error = pcall(view.blocks[block_name], out, vars)
if ok then
return tostring(out)
else
return out:finish(), err.runtime_error(out, error)
end
else
return out:finish(), err.new{
name = name,
message = "Block '" .. block_name .. "' not found"
}
end
end
--- Print template result
--- @param name string|table
--- @param vars table
--- @param chunk_size number
--- @return aspect.output
function template:display(name, vars, chunk_size)
return self:render(name, vars, {
chunk_size = chunk_size,
print = true
})
end
--- The generated data from the template is sent in the reverse order as it is generated.
--- @param name string|table
--- @param vars table
--- @param callback fun(out:aspect.output, chunk:string)
--- @param chunk_size number
--- @return aspect.output
function template:generate(name, vars, callback, chunk_size)
return self:render(name, vars, {
chunk_size = chunk_size,
print = callback
})
end
return template