--https://github.com/mfrigerio17/lua-template-engine/ local function errHandler(e) -- Try to get the number of the line of the template that caused the error, -- parsing the text of the stacktrace. Note that the string here in the -- matching pattern should correspond to whatever is generated in the -- template_eval function, further down local stacktrace = debug.traceback() local linen = tonumber(stacktrace:match('.-"local text={}..."]:(%d+).*')) return { error = e, lineNum = linen, } end --- Evaluate a chunk of code in a constrained environment. -- @param unsafe_code code string -- @param optional environment table. -- @return true or false depending on success -- @return function or error message local function eval_sandbox(unsafe_code, env) local env = env or {} local unsafe_fun, msg = load(unsafe_code, nil, 't', env) if unsafe_fun == nil then return false, { loadError = true, msg = msg } end return xpcall(unsafe_fun, errHandler) end local function lines(s) if s:sub(-1) ~= '\n' then s = s .. '\n' end return s:gmatch('(.-)\n') end --- Copy every string in the second argument into the first, prepending indentation. -- The first argument must be a table. The second argument is either a table -- itself (having strings as elements) or a function returning a factory of -- a suitable iterator; for example, a function returning 'ipairs(t)', where 't' -- is a table of strings, is a valid argument. local insertLines = function(text, lines, totIndent) local factory = lines if type(lines) == 'table' then factory = function() return ipairs(lines) end end for i, line in factory() do local lineadd = '' if line ~= '' then lineadd = totIndent .. line end table.insert(text, lineadd) end end --- Decorates an existing string iteration, adding an optional prefix and suffix. -- The first argument must be a function returning an existing iterator -- generator, such as a 'ipairs'. -- The second and last argument are strings, both optional. -- -- Sample usage: -- local t = {"a","b","c","d"} -- for i,v in ipairs(t) do -- print(i,v) -- end -- -- for i,v in lineDecorator( function() return ipairs(t) end, "--- ", " ###") do -- print(i,v) -- end local lineDecorator = function(generator, prefix, suffix) local opts = opts or {} local prefix = prefix or '' local suffix = suffix or '' local iter, inv, ctrl = generator() return function() local i, line = iter(inv, ctrl) ctrl = i local retline = '' if line ~= nil then if line ~= '' then retline = prefix .. line .. suffix end end return i, retline -- nil or "" end end --- Evaluate the given text-template into a string. -- Regular text in the template is copied verbatim, while expressions in the -- form $() are replaced with the textual representation of , which -- must be defined in the given environment. -- Finally, lines starting with @ are interpreted entirely as Lua code. -- -- @param template the text-template, as a string -- @param env the environment for the evaluation of the expressions in the -- templates (if not given, 'table', 'pairs', 'ipairs' are added -- automatically to this enviroment) -- @param opts non-mandatory options -- - indent: number of blanks to be prepended before every output line; -- this applies to the whole template, relative indentation between -- different lines is preserved -- - xtendStyle: if true, variables are matched with this pattern "«»" -- @return The text of the evaluated template; if the option 'returnTable' is -- set to true, though, the table with the sequence of lines of text is -- returned instead local function template_eval(template, env, opts) local opts = opts or {} local indent = string.format('%s', string.rep(' ', (opts.indent or 0))) -- Define the matching patter for the variables, depending on options. -- The matching pattern reads in general as: local varMatch = { pattern = '(.-)$(%b())()', extract = function(expr) return expr:sub(2, -2) end, } if opts.xtendStyle then varMatch.pattern = '(.-)«(.-)»()' varMatch.extract = function(expr) return expr end end -- Generate a line of code for each line in the input template. -- The lines of code are also strings; we add them in the 'chunk' table. -- Every line is either the insertion in a table of a string, or a 1-to-1 copy -- of the code inserted in the template via the '@' character. local chunk = { 'local text={}' } local lineOfCode = nil for line in lines(template) do -- Look for a '@' ignoring blanks (%s) at the beginning of the line -- If it's there, copy the string following the '@' local s, e = line:find('^%s*@') if s then lineOfCode = line:sub(e + 1) else -- Look for the specials '${..}', which must be alone in the line local tableIndent, tableVarName = line:match('^([%s]*)${(.-)}[%s]*') if tableVarName ~= nil then -- Preserve the indentation used for the "${..}" in the original template. -- "Sum" it to the global indentation passed here as an option. local totIndent = string.format('%q', indent .. tableIndent) lineOfCode = '__insertLines(text, ' .. tableVarName .. ', ' .. totIndent .. ')' else -- Look for the template variables in the current line. -- All the matches are stored as strings '"" .. ' -- Note that can be empty local subexpr = {} local lastindex = 1 local c = 1 for text, expr, index in line:gmatch(varMatch.pattern) do subexpr[c] = string.format('%q .. %s', text, varMatch.extract(expr)) lastindex = index c = c + 1 end -- Add the remaining part of the line (no further variable) - or the -- entire line if no $() was found subexpr[c] = string.format('%q', line:sub(lastindex)) -- Concatenate the subexpressions into a single one, prepending the -- indentation if it is not empty local expression = table.concat(subexpr, ' .. ') if expression ~= '""' and indent ~= '' then expression = string.format('%q', indent) .. ' .. ' .. expression end lineOfCode = 'table.insert(text, ' .. expression .. ')' end end table.insert(chunk, lineOfCode) end local returnTable = opts.returnTable or false if returnTable then table.insert(chunk, 'return text') else -- The last line of code performs string concatenation, so that the evaluation -- of the code eventually leads to a string table.insert(chunk, "return table.concat(text, '\\n')") end --print( table.concat(chunk, '\n') ) env.table = (env.table or table) env.pairs = (env.pairs or pairs) env.ipairs = (env.ipairs or ipairs) env.__insertLines = insertLines local ok, ret = eval_sandbox(table.concat(chunk, '\n'), env) if not ok then local errMessage = 'Error in template evaluation' -- default, should be overwritten if ret.loadError then errMessage = 'Syntactic error in the loaded code: ' .. ret.msg else local linen = ret.lineNum or -1 local line = '??' if linen ~= -1 then line = chunk[linen] end local err1 = 'Template evaluation failed around this line:\n\t>>> ' .. line .. ' (line #' .. linen .. ')' local err2 = 'Interpreter error: ' .. (tostring(ret.error) or '') errMessage = err1 .. '\n' .. err2 end return false, errMessage end return ok, ret end return { template_eval = template_eval, lineDecorator = lineDecorator, }