login

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 sub(s) return 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 computed = line:gsub("{{(.-)}}", sub)
            if #computed > 0 then
                result = result .. computed .. "\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 {render_str=render_str, render_file=render_file}