local function escape(str)
local esc_tab = {["&"]=38, ["<"]=60, [">"]=62, ["{"]=123, ["}"]=125}
local esc = ""
for i = 1, #str do
local c = str:sub(i, i)
local n = esc_tab[c]
if n ~= nil then
c = "&#" .. n .. ";"
end
esc = esc .. c
end
return esc
end
local function eval(str, env)
local expr = str:gsub("%$(%w+)", "env.%1")
local code = "return function(env) return "..expr.." end"
return loadstring(code, "eval")()(env)
end
local function find_matching_end(lines, start, last)
local level = 1
local else_line
for line_num = start+1, last do
local line = lines[line_num]
if line:sub(1, 1) == "%" then
if line:match("%%%s+end") then
level = level - 1
if level == 0 then
return line_num, else_line
end
elseif line:match("%%%s+else") then
if level == 1 then
else_line = line_num
end
elseif line:match("%%%s+set") == nil then
level = level + 1
end
end
end
error(("line %d: no matching end"):format(start))
end
local function render_block(lines, first, last, env)
local result = ""
local function raw_sub(s) return eval(s, env) end
local function esc_sub(s) return escape(tostring(eval(s, env))) end
local line_num = first
while line_num <= last do
local line = lines[line_num]
if line:match("%%%s*(%w+).*") == "set" then
local var, expr = line:match("%%%s*set%s+([%w_]+)%s*=%s*(.+)")
assert(var and expr)
env[var] = eval(expr, env)
line_num = line_num + 1
elseif line:sub(1, 1) == "%" then -- block start
local block_type = line:match("%%%s*(%w+).*")
local block_end, else_line = find_matching_end(lines, line_num, last)
if block_type == "if" then
local expr = line:match("%%%s*if%s+(.+)%s+then")
assert(expr)
if eval(expr, env) then
local true_end = else_line or block_end
result = result .. render_block(lines, line_num+1, true_end-1, env)
elseif else_line ~= nil then
result = result .. render_block(lines, else_line+1, block_end-1, env)
end
elseif block_type == "while" then
local expr = line:match("%%%s*while%s+(.+)%s+do")
assert(expr)
while eval(expr, env) do
result = result .. render_block(lines, line_num+1, block_end-1, env)
end
elseif block_type == "for" then
local var, expr = line:match("%%%s*for%s+([%w_]+)%s+in%s+(.+)%s+do")
assert(var and expr)
for i, v in ipairs(eval(expr, env)) do
env[var] = v
result = result .. render_block(lines, line_num+1, block_end-1, env)
end
end
line_num = block_end + 1
else
local line = line:gsub("{{([^!].-)}}", esc_sub)
local line = line:gsub("{{!(.-)}}", raw_sub)
if #line > 0 then
result = result .. line .. "\n"
end
line_num = line_num + 1
end
end
return result
end
local function render_str(str, env)
assert(str:sub(#str) == "\n")
local lines = {}
for line in str:gmatch("(.-)\r?\n") do
line = line:gsub("^%s*%%", "%%")
table.insert(lines, line)
end
return render_block(lines, 1, #lines, env)
end
local function render_file(fname, env)
local str = io.input(fname):read("*a")
return render_str(str, env)
end
return {escape=escape, render_str=render_str, render_file=render_file}