--[[ MARK: - Template Engine Builds a formspec from the contents of a game-provided main menu file. ================================================================================= Menu File Documentation ================================================================================= Menu files are essentially just formspecs, but with special semantics and extended syntax. First, menu files are made up of a number of dialog definitions. A dialog definition is basically an HTML tag, where the tag name is the name of the dialog. For example this: ```
label[2,2;A label]
button[1,1;4,1;button;This is a button] ``` will define a dialog named 'main', with a label in it, and a dialog named 'other', with a button in it. Every menu file must have a dialog named 'main', which serves as the entry point of the game's menu. Note that to prevent dialog definitions from conflicting with the contents of hypertext[] elements, they must not have leading whitespace before the opening delimiter. Being able to define these other dialogs would be pretty pointless if they couldn't be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons, to segue to a different dialog. To do this, set the action name to '.show_dialog_', where is the name of the dialog you want to open. You can also use '.overlay_dialog_' to draw the target dialog on top of the current dialog, and '.unoverlay_dialog' to hide it again. There are several other action patterns that invoke special behavior: - - '.play': Start the game. - '.play_with_world_named_': Start the game on the specified world. If the world in question does not exist, it will be created. It is also possible to define metadata for the main menu. To do this, create a dialog section named 'meta'. The contents of this section will then be parsed in the same way as minetest.conf instead of being treated as a dialog. Available metadata options are: - 'enable_clouds': true/false (defaults to false) Now, all this is good when we know exactly what content we want on the main menu. But what if we want to render e.g. a list of worlds, or even a list of game modes? This can be done using the @foreach construct. @foreach takes in a list of values, the name of a JSON file, or a reference to a menu-exposed list. The major advantage of a foreach loop, however, is interpolation. Inside a foreach loop, the pattern '${}' will be replaces with the result of . Inside an expression, you can reference variables as '@'. Which variables are available depends on the iterated list. In the case of a list literal, the current item is exposed as a variable named @item, while for a JSON file, each key in the file's top-level object will correspond to a variable. (Note that variable names may only contain letters or numbers, even if mapped from a JSON file.) The menu-provided lists are: - @WORLDS: The list of worlds for this game. Exposes @name (the world name), @path (the world's full absolute path), @gameid (the ID of the current game), and @selected (whether the world is currently selected). - @MODS: The list of installed mods that are not incompatible with this game. Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends. - @WORLDMODS: The list of mods installed on the current world. No-op if no world is selected. Expressions also support rudimentary mathematical operations, namely addition (+), 'subtraction' (+-), multiplication (*), division (/), and exponentiation (^). Trying to perform math on a non-tonumber()-able variable will treat the variable as 0. As an example, this: ``` label[1,1;Worlds:] @foreach:$WORLDS:name label[1,${@i + 1};World named ${@name}] @endforeach:name @foreach:[one,two,three]:name2 label[4,${@i + 1};Item ${@i} is named: ${@item}] @endforeach:name2 ``` will: - Create a "Worlds" label at 1,1; - Create a label for every world with that world's name and a Y coordinate that corresponds to the world's position in the list, plus 1 so these labels start below the existing label. - Create labels for "one", "two", and "three" that display the item's value and its position in the list, with similarly increasing Y-coordinates. Besides foreach loops, menu files also support conditionals. Conditionals are written as '@if:: ... @else: ... @endif:', where is any expression. The conditional will be replaced with the contents of its first block if the condition evaluates to non-zero, and the contents of its second block otherwise. Note that the second block may not be omitted due to how the parser works. Example: ``` @if: @name = test : @else: @endif: ``` Notes: - The only uniqueness requirements for the name of a block is that it not be the name of a statment of the same type contained in the body of that block. This is so that the parser knows which `end` belongs to which block without having to manage state. Note: Because of the way the main menu works, image paths must be specified in full. To make this non-painful, when referencing images use '$ASSET_PATH/' instead of just the image name. $ASSET_PATH will be replaced with the actual path to the game's menu/ directory, so images used by the main menu should be stored there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH is the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in case you need to stylistically unset a background image defined by a global style. --]] local zone = function() end if not minetest then zone = require("jit.zone") minetest = { formspec_escape = function() end, hypertext_escape = function() end, get_translator = function() return function() end, function() end end } local function basic_dump(o) local tp = type(o) if tp == "number" then local s = tostring(o) if tonumber(s) == o then return s end -- Prefer an exact representation over a compact representation. -- e.g. basic_dump(0.3) == "0.3", -- but basic_dump(0.1 + 0.2) == "0.30000000000000004" -- so the user can see that 0.1 + 0.2 ~= 0.3 return string.format("%.17g", o) elseif tp == "string" then return string.format("%q", o) elseif tp == "boolean" then return tostring(o) elseif tp == "nil" then return "nil" elseif tp == "userdata" then return tostring(o) else return string.format("<%s>", tp) end end local keywords = { ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, ["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true, ["function"] = true, ["goto"] = true, -- Lua 5.2 ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true, ["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true, } local function is_valid_identifier(str) if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then return false end return true end -------------------------------------------------------------------------------- -- Dumps values in a line-per-value format. -- For example, {test = {"Testing..."}} becomes: -- _["test"] = {} -- _["test"][1] = "Testing..." -- This handles tables as keys and circular references properly. -- It also handles multiple references well, writing the table only once. -- The dumped argument is internal-only. function dump2(o, name, dumped) name = name or "_" -- "dumped" is used to keep track of serialized tables to handle -- multiple references and circular tables properly. -- It only contains tables as keys. The value is the name that -- the table has in the dump, eg: -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]' dumped = dumped or {} if type(o) ~= "table" then return string.format("%s = %s\n", name, basic_dump(o)) end if dumped[o] then return string.format("%s = %s\n", name, dumped[o]) end dumped[o] = name -- This contains a list of strings to be concatenated later (because -- Lua is slow at individual concatenation). local t = {} for k, v in pairs(o) do local keyStr if type(k) == "table" then if dumped[k] then keyStr = dumped[k] else -- Key tables don't have a name, so use one of -- the form _G["table: 0xFFFFFFF"] keyStr = string.format("_G[%q]", tostring(k)) -- Dump key table t[#t + 1] = dump2(k, keyStr, dumped) end else keyStr = basic_dump(k) end local vname = string.format("%s[%s]", name, keyStr) t[#t + 1] = dump2(v, vname, dumped) end return string.format("%s = {}\n%s", name, table.concat(t)) end -- This dumps values in a human-readable expression format. -- If possible, the resulting string should evaluate to an equivalent value if loaded and executed. -- For example, {test = {"Testing..."}} becomes: -- [[{ -- test = { -- "Testing..." -- } -- }]] function dump(value, indent) indent = indent or "\t" local newline = indent == "" and "" or "\n" local rope = {} local write do -- Keeping the length of the table as a local variable is *much* -- faster than invoking the length operator. -- See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12. local i = 0 function write(str) i = i + 1 rope[i] = str end end local n_refs = {} local function count_refs(val) if type(val) ~= "table" then return end local tbl = val if n_refs[tbl] then n_refs[tbl] = n_refs[tbl] + 1 return end n_refs[tbl] = 1 for k, v in pairs(tbl) do count_refs(k) count_refs(v) end end count_refs(value) local refs = {} local cur_ref = 1 local function write_value(val, level) if type(val) ~= "table" then write(basic_dump(val)) return end local tbl = val if refs[tbl] then write(refs[tbl]) return end if n_refs[val] > 1 then refs[val] = ("getref(%d)"):format(cur_ref) write(("setref(%d)"):format(cur_ref)) cur_ref = cur_ref + 1 end write("{") if next(tbl) == nil then write("}") return end write(newline) local function write_entry(k, v) write(indent:rep(level)) write("[") write_value(k, level + 1) write("] = ") write_value(v, level + 1) write(",") write(newline) end local keys = {string = {}, number = {}} for k in pairs(tbl) do local t = type(k) if keys[t] then table.insert(keys[t], k) end end -- Write string-keyed entries table.sort(keys.string) for _, k in ipairs(keys.string) do local v = val[k] if is_valid_identifier(k) then write(indent:rep(level)) write(k) write(" = ") write_value(v, level + 1) write(",") write(newline) else write_entry(k, v) end end -- Write number-keyed entries local len = 0 for i in ipairs(tbl) do len = i end if #keys.number == len then -- table is a list for _, v in ipairs(tbl) do write(indent:rep(level)) write_value(v, level + 1) write(",") write(newline) end else -- table harbors arbitrary number keys table.sort(keys.number) for _, k in ipairs(keys.number) do write_entry(k, tbl[k]) end end -- Write all remaining entries for k, v in pairs(val) do if not keys[type(k)] then write_entry(k, v) end end write(indent:rep(level - 1)) write("}") end write_value(value, 1) return table.concat(rope) end function string.split(str, delim, include_empty, max_splits, sep_is_pattern) delim = delim or "," if delim == "" then error("string.split separator is empty", 2) end max_splits = max_splits or -2 local items = {} local pos, len = 1, #str local plain = not sep_is_pattern max_splits = max_splits + 1 repeat local np, npe = string.find(str, delim, pos, plain) np, npe = (np or (len+1)), (npe or (len+1)) if (not np) or (max_splits == 1) then np = len + 1 npe = np end local s = string.sub(str, pos, np - 1) if include_empty or (s ~= "") then max_splits = max_splits - 1 items[#items + 1] = s end pos = npe + 1 until (max_splits == 0) or (pos > (len + 1)) return items end end local fe = minetest.formspec_escape local hte = minetest.hypertext_escape template = {} local operators = { ["or"] = {prec = 0, assoc = "left", kind = "binary"}, ["and"] = {prec = 1, assoc = "left", kind = "binary"}, ["=="] = {prec = 2, assoc = "left", kind = "binary"}, ["<"] = {prec = 2, assoc = "left", kind = "binary"}, ["<="] = {prec = 2, assoc = "left", kind = "binary"}, [">"] = {prec = 2, assoc = "left", kind = "binary"}, [">="] = {prec = 2, assoc = "left", kind = "binary"}, ["+"] = {prec = 3, assoc = "left", kind = "binary"}, ["-"] = {prec = 3, assoc = "left", kind = "binary"}, ["*"] = {prec = 4, assoc = "left", kind = "binary"}, ["/"] = {prec = 4, assoc = "left", kind = "binary"}, ["%"] = {prec = 5, assoc = "left", kind = "binary"}, ["not"] = {prec = 9, kind = "unary"}, ["^"] = {prec = 10, assoc = "right", kind = "binary"}, -- Add more operators here, e.g. ["=="] = {prec = 0, assoc = "left"} for comparisons } function template.build_expr(expr) -- First pass: Collect tokens into a flat list local out = {} local pos = 1 local len = #expr while pos <= len do local matched = false -- Skip whitespace local ws_start, ws_end = expr:find("^%s+", pos) if ws_start then pos = ws_end +1 matched = true end -- Strings if not matched then local str_start, str_end, str = expr:find('^"([^"]+)"', pos) if not str_start then str_start, str_end, str = expr:find("^'([^']+)'", pos) end if str_start then out[#out +1] = {type = "string", value = str} pos = str_end +1 matched = true end end -- Numbers if not matched then local num_start, num_end, num = expr:find("^(%d+%.?%d*[eE]?[+-]?%d*)", pos) if num_start then out[#out +1] = {type = "number", value = tonumber(num)} pos = num_end +1 matched = true end end -- Operators if not matched then for op in pairs(operators) do local op_start = pos local op_end = pos +op:len() -1 if expr:sub(op_start, op_end) == op then out[#out +1] = {type = "operator", op = op} pos = op_end +1 matched = true end end end -- Grouping operators if not matched then local op_start, op_end, op = expr:find("^(%p)", pos) if op_start then if op == "," then out[#out +1] = {type = "comma"} elseif op == "." then out[#out +1] = {type = "dot"} elseif op == "(" then out[#out +1] = {type = "lparen"} elseif op == ")" then out[#out +1] = {type = "rparen"} elseif op == "[" then out[#out +1] = {type = "lbracket"} elseif op == "]" then out[#out +1] = {type = "rbracket"} else goto aaa end pos = op_end +1 matched = true ::aaa:: end end -- Keywords if not matched then if expr:find("^true") then out[#out +1] = {type = "bool", value = true} pos = pos +3 matched = true elseif expr:find("^false") then out[#out +1] = {type = "bool", value = false} pos = pos +4 matched = true end end -- Identifiers if not matched then local id_start, id_end, id = expr:find("^([%a_][%w_]*)", pos) if id_start then local item = {type = "identifier", name = id} pos = id_end +1 out[#out +1] = item matched = true end end if not matched then pos = pos +1 end end -- Second pass: Fold the flat list into an expression tree local tree = {} local cursor = 1 local peek, consume, expect, parse_expr, parse_primary, parse_postfix function peek() return out[cursor] end function consume() cursor = cursor +1 return out[cursor -1] end function expect(ty) local t = consume() if not t or t.type ~= ty then error("Expected " .. ty .. " but got " .. (t and t.type or "nil").."(in expression `"..expr.."`)") end end parse_expr = function(min_prec) min_prec = min_prec or 0 -- Parse left operand: handle unary prefix operators local left local token = peek() if token and token.type == "operator" then local op_info = operators[token.op] if op_info and op_info.kind == "unary" then -- For unary prefix, use a high binding power consume() -- Recurse with high precedence for operand local operand = parse_expr(op_info.prec) left = {type = "unary_operator", op = token.op, expr = operand} else left = parse_primary() end else left = parse_primary() end -- Apply postfix operators (index, call) - highest precedence left = parse_postfix(left) -- Handle binary operators (infix) while true do local token = peek() if not token or token.type ~= "operator" then break end local op_info = operators[token.op] if not op_info or op_info.kind ~= "binary" or op_info.prec < min_prec then break end consume() local next_min_prec = op_info.prec if op_info.assoc == "left" then next_min_prec = next_min_prec + 1 end -- For right-assoc, use >= so same prec binds right local right = parse_expr(next_min_prec) left = {type = "binary_operator", op = token.op, lhs = left, rhs = right} end return left end -- Parse postfix operators (index, call) parse_postfix = function(left) while peek() do local t = peek() if t.type == "lbracket" then -- Indexing has high precedence consume() local key = parse_expr(0) -- Inside [] can have full expressions expect("rbracket") left = {type = "index", base = left, key = key} elseif t.type == "lparen" then -- Function call consume() local args = {} while peek().type ~= "rparen" do local e = parse_expr(0) args[#args +1] = e if peek().type == "comma" then consume() end end expect("rparen") left = {type = "call", func = left, args = args} elseif t.type == "dot" then consume() if peek().type == "identifier" then left = {type = "index", base = left, key = {type = "string", value = consume().name}} else -- Syntax error end else break end end return left end parse_primary = function() local t = peek() if t.type == "number" or t.type == "string" or t.type == "bool" or t.type == "list" then consume() return t elseif t.type == "identifier" then consume() -- if peek() and peek().type == "lparen" then -- consume() -- local args = {} -- while peek().type ~= "rparen" do -- local e = parse_expr(0) -- args[#args +1] = e -- if peek().type == "comma" then consume() end -- end -- -- return {type = "call", func = t, args = args} -- end return t elseif t.type == "lbracket" then consume() local list = {} while peek().type ~= "rbracket" do local e = parse_expr(0) list[#list +1] = e if peek().type == "comma" then consume() end end return {type = "list", value = list} elseif t.type == "lparen" then consume() local e = parse_expr(0) expect("rparen") return e else print("Unexpected token: "..consume().type) end end return parse_expr(0) end function template.build(menu) local _id = 0 local function id() _id = _id +1 return _id end local out = {["@root"] = {}} local view_name = "main" local block = {parent = {}, id="_root"} local item = {parent = {}, id="_root"} local offset = 1 local pos = 1 while true do local matched = false local stmt_start, stmt_end, stmt = menu:find("{%%%s*(.-)%s*%%}", pos) local expr_start, expr_end, expr = menu:find("{{(.-)}}", pos) local comment_start, comment_end, comment = menu:find("{#(.-)#}", pos) -- Statements if stmt_start and (not expr_start or stmt_start < expr_start) and (not comment_start or stmt_start < comment_start) then block[#block +1] = menu:sub(offset, stmt_start -1) local verb = stmt:match "^(%a+)" if verb == "if" then item = { id = id(), type = "if", parent = block, conditions = {{condition = template.build_expr(stmt:match "if%s*(.*)"), body = {}}} } block[#block +1] = item block = item.conditions[1].body block.parent = item if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end elseif verb == "elseif" then local x = { condition = template.build_expr(stmt:match "if%s*(.*)"), body = {} } item.conditions[#item.conditions +1] = x block = x.body block.parent = item if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end elseif verb == "else" then local x = { condition = {type = "bool", value = true}, body = {} } item.conditions[#item.conditions +1] = x block = x.body block.parent = item if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end elseif verb == "endif" then block.parent = nil block = item.parent item.parent = nil item = block.parent elseif verb == "for" then local var, iter = stmt:match "for%s*([%a_][%w_]*)%s*in%s*(.*)" item = { id = id(), type = "for", parent = block, varname = var, iterate = template.build_expr(iter), body = {} } block[#block +1] = item block = item.body block.parent = item elseif verb == "endfor" then block.parent = nil block = item.parent item.parent = nil item = block.parent elseif verb == "macro" then local name, params = stmt:match "macro%s*([%a_][%w_]*)%s*(.*)" item = { type = "macro", parent = block, name = name, params = params:match("^%((.*)%)$"):split("%s*,%s*", false, -1, true), body = {} } block[#block +1] = item block = item.body block.parent = item elseif verb == "endmacro" then block.parent = nil block = item.parent item.parent = nil item = block.parent elseif verb == "set" then local name, expr = stmt:match "set %s*([%a_][%w_]*)%s*=?%s*(.*)" if expr and expr ~= "" then block[#block +1] = {type = "set", name = name, value = template.build_expr(expr)} else item = { type = "set_block", parent = block, name = name, value = {} } block[#block +1] = item block = item.value block.parent = item end elseif verb == "endset" then block = item.parent item.parent = nil item = block.parent block.parent = nil elseif verb == "include" then elseif verb == "view" then view_name = stmt:match "view%s*([%a_][%w_]*)%s*" or "main" table.insert_all(out["@root"], block) item = {id = "root"} block = {id = "root"} elseif verb == "endview" then out[view_name] = block block = {} end offset = stmt_end +1 pos = stmt_end matched = true end -- Interpolations if expr_start and expr_start < (stmt_start or math.huge) and expr_start < (comment_start or math.huge) then block[#block +1] = menu:sub(offset, expr_start -1) block[#block +1] = template.build_expr(expr) offset = expr_end +1 pos = expr_end matched = true end -- Comments if comment_start and comment_start < (stmt_start or math.huge) and comment_start < (expr_start or math.huge) then block[#block +1] = menu:sub(offset, comment_start -1) offset = comment_end +1 pos = comment_end matched = true end if not matched then break end end block[#block +1] = menu:sub(offset) out[#out +1] = block -- Attach a reference to the root scope in each view. for k, v in pairs(out) do if k ~= "@root" then v.root = out["@root"] end end return out end function template.evaluate_expr(expr, vars) assert(expr, "No expression given!") if expr.type == "binary_operator" then assert(expr.lhs and expr.rhs, "Binary operator missing an operand") if expr.op == "+" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) + (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "*" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) * (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "-" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) - (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "/" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) / (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "%" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) % (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "or" then return template.evaluate_expr(expr.lhs, vars) or template.evaluate_expr(expr.rhs, vars) elseif expr.op == "and" then return template.evaluate_expr(expr.lhs, vars) and template.evaluate_expr(expr.rhs, vars) elseif expr.op == "==" then return template.evaluate_expr(expr.lhs, vars) == template.evaluate_expr(expr.rhs, vars) elseif expr.op == ">" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) > (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == ">" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) >= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "<" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) < (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) elseif expr.op == "<=" then return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) <= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0) else return template.evaluate_expr(expr.lhs, vars) end elseif expr.type == "unary_operator" then assert(expr.expr, "Invalid unary expression") if expr.op == "not" then return not template.evaluate_expr(expr.expr, vars) else return template.evaluate_expr(expr.expr, vars) end elseif expr.type == "identifier" then return vars[expr.name] elseif expr.type == "index" then assert(expr.base and expr.key, "Invalid indexing") return template.evaluate_expr(expr.base, vars)[template.evaluate_expr(expr.key, vars)] elseif expr.type == "call" then assert(expr.func, "Invalid function ref") local fn = template.evaluate_expr(expr.func, vars) if type(fn) == "function" then local args = {} for _, x in pairs(expr.args) do assert(x, "Invalid argument") args[#args +1] = template.evaluate_expr(x, vars) end return fn(unpack(args)) end elseif expr.type == "list" then local out = {} for i, x in ipairs(expr.value) do assert(x, "Invalid list item") out[i] = template.evaluate_expr(x, vars) end return out else return expr.value or false end end local S, PS = minetest.get_translator() local env = { S = S, translate = S, PS = S, translate_n = PS, fe = fe, formspec_escape = fe, hte = hte, hypertext_escape = hte, DEFAULT_ASSETS = assets, floor = math.floor, ceil = math.ceil, round = math.round, min = math.min, max = math.max, -- Convert arguments to a formspec-safe representation that can be evaluated in on_reveive_fields action = function(type, ...) local args = {...} if type == "set" then local name = args[1] return ".set_|"..fe(name).."|_to_"..fe(minetest.write_json(args[2])) end return out end, cycle = function(idx, ...) local options = {...} return options[((idx -1) %#options) +1] end, upper = function(str) return string.upper(str) end, len = function(x) return #x end, range = function(min, max, step) local out = {} if not step then step = 1 end for i = min, max, step do out[#out +1] = i end return out end } function template.evaluate(dialog, vars, depth) -- Ensure that variables from `env` remain accessible even when `vars` has its own metatable set. local out if not depth then local real_vars = vars vars = setmetatable({}, { __index = function(tbl, k) return real_vars[k] or env[k] end }) depth = 0 out = dialog.root and {template.evaluate(dialog.root, vars, depth +1)} or {} else out = {} end for _, item in ipairs(dialog) do if type(item) == "string" then out[#out +1] = item else if item.type == "if" then for _, cond in ipairs(item.conditions) do if template.evaluate_expr(cond.condition, vars) then out[#out +1] = template.evaluate(cond.body, vars, depth +1) break end end elseif item.type == "for" then local iterable = template.evaluate_expr(item.iterate, vars) if type(iterable) == "table" then for i, entry in ipairs(iterable) do local vars2 = { index = i, index0 = i -1, length = #iterable, first = i == 1, depth = 1, previtem = iterable[i -1], nextitem = iterable[i +1], cycle = function(...) return env.cycle(i, ...) end } vars2.last = i == vars2.length vars2.revindex = vars2.length -vars2.index vars2.revindex0 = vars2.length -vars2.index0 if vars.loop and vars.loop.depth then vars.depth = vars.loop.depth +1 end -- Save the original values of these variables, to simulate a closure. local prev_var = vars[item.varname] local prev_loop = vars.loop vars[item.varname] = entry vars.loop = vars2 out[#out +1] = template.evaluate(item.body, vars, depth +1) -- Restore the variables we set to their original values. vars[item.varname] = prev_var vars.loop = prev_loop end end elseif item.type == "set" then vars[item.name] = template.evaluate_expr(item.value, vars) elseif item.type == "set_block" then vars[item.name] = template.evaluate(item.value, vars, depth +1) elseif item.type == "macro" then vars[item.name] = function(...) local argv = {...} local args = {} for i, x in ipairs(argv) do args[item.params[i]] = x end return template.evaluate(item.body, setmetatable(args, {__index = vars}), depth +1) end else out[#out + 1] = tostring(template.evaluate_expr(item, vars)) end end end return table.concat(out) end function template.test() -- local profile = require("jit.profile") -- local profile_data = {} -- local function profile_callback(thread, samples, vmstate) -- profile_data[#profile_data +1] = {profile.dumpstack(thread, "pF;", -100), vmstate, -- " ", samples, "\n"} -- end -- Start profiling -- profile.start("li0.1", profile_callback) local ast = template.build [[ {% macro field(x, y, w, h, name, value) %} image[{{ x }},{{ y }};{{ w }},{{ h }};{{ DEFAULT_ASSETS }}btn_bg_2_light.png;8,8] field[{{ x + 0.1 }},{{ y }};{{ w - 0.2 }},{{ h }};{{ name }};;{{ value }}] {% endmacro %} {% view main %} {% if selected_world %} {% set list_width = WIDTH * 0.3 %} {% else %} {% set list_width = WIDTH * 0.8 %} {% endif %} {{ field(2, 2, 4, 0.75, 'test', '') }} {% set col = 12 % 3 %} {{ col > 0 and 'yes' or 'no' }} {{ action("set", "q", [1, 2, 3]) }} scroll_container[0,0;{{ foo }},{{ foo }};worldscroll;vertical;;1,0] {# image[{{ WIDTH * 0.1 - 0.1 }},{{ HEIGHT * 0.1 - 0.1}};{{ WIDTH * 0.8 + 0.2 }},{{ HEIGHT * 0.8 + 0.2 }};{{ DEFAULT_ASSETS }}bg_translucent.png;8,8] scroll_container[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.1 }};{{ list_width }},{{ HEIGHT * 0.8 }};worldscroll;vertical;;0,0] {% for world in get_worlds() %} style[.select_world_{{ world.path }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_world_{{ world.path }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_world_{{ world.path }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] button[0,{{ loop.index0 * 0.5}};{{ list_width }},0.5;.select_world_{{ world.path }};{{ world.name }}] {% endfor %} scroll_container_end[] {% if selected_world %} box[{{ WIDTH * 0.4 - 0.05 }},{{ HEIGHT * 0.1 - 0.1 }};0.1,{{ HEIGHT * 0.8 + 0.2 }};#292d2fff] scroll_container[{{ WIDTH * 0.4 + 0.05 }},{{ HEIGHT * 0.1 }};{{ WIDTH * 0.8 - 0.05 }},{{ HEIGHT * 0.8 }};worlconfigscroll;vertical;;0,0] {% set j = 0 %} {% if setting_damage %} {% if damage_enabled %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_damage_enabled_to_false;] {% else %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_damage_enabled_to_true;] {% endif %} label[0.5,{{ j + 0.25 }};Damage] {% set j = j + 0.5 %} {% endif %} {% if setting_creative %} {% if creative_enabled %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_creative_enabled_to_false;] {% else %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_creative_enabled_to_true;] {% endif %} label[0.5,{{ j + 0.25 }};Creative] {% set j = j + 0.5 %} {% endif %} {% set play_str = "Play" %} {% if setting_server %} {% if server_enabled %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_server_enabled_to_false;] {% set play_str = "Host server" %} {% else %} image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_server_enabled_to_true;] {% set play_str = "Play" %} {% endif %} label[0.5,{{ j + 0.25 }};Server] {% set j = j + 0.5 %} {% endif %} {% set j = j + 1 %} button[{{ WIDTH * 0.05 }},{{ j }};{{ WIDTH * 0.4 }},0.75;.overlay_view_modconfig;Configure mods] button[{{ WIDTH * 0.05 }},{{ j + 1 }};{{ WIDTH * 0.4 }},0.75;.play;{{ play_str }}] scroll_container_end[] scrollbar[-800,6;0,2;vertical;worldconfigscroll;] {% endif %} scrollbaroptions[arrows=hide] scrollbar[-800,6;0,2;vertical;worldscroll;] {% endview %} {% view modconfig %} image[{{ WIDTH * 0.05 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8] hypertext[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;Available mods] scroll_container[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};modsscroll;vertical;;0,0] {% for mod in get_mods() %} style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}] {% endfor %} scroll_container_end[] scrollbar[-800,6;0,2;vertical;modsscroll;] image[{{ WIDTH * 0.55 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8] hypertext[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;Enabled mods] scroll_container[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};worldmodsscroll;vertical;;0,0] {% for mod in get_world_mods() %} {% if not mod.game_provided %} style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}] button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}] {% endif %} {% endfor %} scroll_container_end[] scrollbar[-800,6;0,2;vertical;worldmodsscroll;] image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 - WIDTH * 0.1 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_right.png;.add_mod_to_world;] image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_left.png;.remove_mod_from_world;] tooltip[.add_mod_to_world;Add mod to world;#444;#aaa] tooltip[.remove_mod_from_world;Remove mod from world;#444;#aaa] #} button[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Cancel] button[{{ WIDTH * 0.5 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Confirm] {% endview %} {% view addworld %} {% endview %} ]] -- print(dump(ast, " ")) print(template.evaluate(ast.main, setmetatable({z = false, bar = true, foo = 3, blah = "test"}, {__index = {q = "hello"}}))) -- profile.stop() -- -- print("Profiler data: "..dump(profile_data, " ")) -- -- -- Analyze results -- for stack, samples in pairs(profile_data) do -- print(string.format("%s: %d samples", stack, samples)) -- end end local time = os.clock() --for _ = 1, 1000 do template.test() --end print("Execution finished in: "..(os.clock() -time))