Initial commit
This commit is contained in:
commit
200e3f7642
46 changed files with 2574 additions and 0 deletions
589
templates.lua
Normal file
589
templates.lua
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
--[[ 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:
|
||||
```
|
||||
<main>
|
||||
label[2,2;A label]
|
||||
</main>
|
||||
<other>
|
||||
button[1,1;4,1;button;This is a button]
|
||||
</other>
|
||||
```
|
||||
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_<name>', where <name> is the name of the dialog you want to open.
|
||||
You can also use '.overlay_dialog_<name>' 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_<name>': 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 '${<expression>}' will be replaces with the result of <expression>.
|
||||
|
||||
Inside an expression, you can reference variables as '@<name>'. 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:<condition>:<name> ... @else:<name> ... @endif:<name>', where
|
||||
<condition> 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 :<name>
|
||||
<formspec 1>
|
||||
@else:<name>
|
||||
<formspec 2>
|
||||
@endif:<name>
|
||||
```
|
||||
|
||||
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/<image name>'
|
||||
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 fe = minetest.formspec_escape
|
||||
local hte = minetest.hypertext_escape
|
||||
|
||||
local function build_template_dialog(fs, depth)
|
||||
if fs:trim() == "" then return end
|
||||
local dialog = {}
|
||||
local i = 0
|
||||
-- "(.-\n?)%s-@foreach:([^:]+):(%l*)\n(.-)\n%s-@endforeach:%3(\n?.*)"
|
||||
-- Extract foreach loops
|
||||
local prev = 0
|
||||
while i < 1000 do
|
||||
local fe_start, fe_end, fe_pattern, fe_name, fe_content = fs:find("@foreach:([^:]+):(%w-)\n(.-)\n%s-@endforeach:%2", prev)
|
||||
local if_start, if_end, if_expr, if_name, if_content, else_content = fs:find("@if:([^:]+):(%w-)\n(.-)\n%s-@else:%2\n(.-)\n%s-@endif:%2", prev)
|
||||
-- print(string.rep("-", 20)..(depth or 0))
|
||||
if not fe_start and not if_start then break end
|
||||
if fe_start and fe_start < (if_start or math.huge) then
|
||||
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
|
||||
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, fe_start -1), (depth or 0) +1)
|
||||
dialog[#dialog +1] = {
|
||||
foreach = fe_pattern:trim(),
|
||||
name = fe_name,
|
||||
content = build_template_dialog(fe_content:trim(), (depth or 0) +1)
|
||||
}
|
||||
prev = fe_end
|
||||
i = i +1
|
||||
end
|
||||
|
||||
if if_start and if_start < (fe_start or math.huge) then
|
||||
-- print("if "..expr.." ("..name..")\n"..content.."\nend if ("..name..")")
|
||||
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, if_start -1), (depth or 0) +1)
|
||||
dialog[#dialog +1] = {
|
||||
condition = if_expr:trim(),
|
||||
name = if_name,
|
||||
content = build_template_dialog(if_content:trim(), (depth or 0) +1),
|
||||
else_content = build_template_dialog(else_content:trim(), (depth or 0) +1)
|
||||
}
|
||||
prev = if_end
|
||||
i = i +1
|
||||
end
|
||||
end
|
||||
|
||||
if prev > 1 then
|
||||
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1))
|
||||
end
|
||||
|
||||
if #dialog == 1 then dialog = dialog[1] end
|
||||
if i == 0 then dialog = fs end
|
||||
-- minetest.log(dump(dialog, " "))
|
||||
return dialog
|
||||
end
|
||||
|
||||
function build_game_menu(input)
|
||||
-- MARK: Environment variables
|
||||
input = "\n"..input
|
||||
:gsub("%$ASSET_PATH", fe(state.current_game.path.."/menu"))
|
||||
:gsub("%$DEFAULT_ASSET_PATH", fe(assets:sub(1, #assets -1)))
|
||||
:gsub("%$NO_IMAGE", fe(default_textures.."blank.png"))
|
||||
:gsub("/%*.-%*/", "")
|
||||
local menu = {}
|
||||
for name, fs in input:gmatch("%f[^\n]<(%l+)>(.-)%f[^\n]</%1>") do
|
||||
if name == "meta" then
|
||||
menu[name] = parse_conf_text(fs)
|
||||
else
|
||||
menu[name] = build_template_dialog(fs)
|
||||
end
|
||||
end
|
||||
-- print(dump(menu, " "))
|
||||
return menu
|
||||
end
|
||||
|
||||
|
||||
-- Split a template expression into a binary-tree node.
|
||||
local function split_template_expression(expr)
|
||||
|
||||
-- These are basically the operator definitions, listed from lowest
|
||||
-- (evaluated last) to highest (evaluated first) precedence.
|
||||
|
||||
local a, b, op
|
||||
-- Or
|
||||
if not a then
|
||||
a, b, op = expr:find("(|)")
|
||||
end
|
||||
-- And
|
||||
if not a then
|
||||
a, b, op = expr:find("(&)")
|
||||
end
|
||||
-- Greater than
|
||||
if not a then
|
||||
a, b, op = expr:find("(>)")
|
||||
end
|
||||
-- Greater than or equal to
|
||||
if not a then
|
||||
a, b, op = expr:find("(>=)")
|
||||
end
|
||||
-- Less than
|
||||
if not a then
|
||||
a, b, op = expr:find("(<)")
|
||||
end
|
||||
-- Less than or equal to
|
||||
if not a then
|
||||
a, b, op = expr:find("(<=)")
|
||||
end
|
||||
-- Equal to
|
||||
if not a then
|
||||
a, b, op = expr:find("(==)")
|
||||
end
|
||||
if not a then
|
||||
a, b, op = expr:find("(=)")
|
||||
end
|
||||
|
||||
-- Addition
|
||||
if not a then
|
||||
a, b, op = expr:find("(+)")
|
||||
end
|
||||
-- Multiplication
|
||||
if not a then
|
||||
a, b, op = expr:find("(*)")
|
||||
end
|
||||
-- Division
|
||||
if not a then
|
||||
a, b, op = expr:find("(/)")
|
||||
end
|
||||
-- Modulo
|
||||
if not a then
|
||||
a, b, op = expr:find("(%%)")
|
||||
end
|
||||
-- Exponent
|
||||
if not a then
|
||||
a, b, op = expr:find("(%^)")
|
||||
end
|
||||
|
||||
if not a then
|
||||
return {value = expr}
|
||||
end
|
||||
|
||||
return {
|
||||
op = op,
|
||||
lhs = split_template_expression(expr:sub(1, a -1)),
|
||||
rhs = split_template_expression(expr:sub(b +1))
|
||||
}
|
||||
end
|
||||
|
||||
-- Reduce a template expression from a binary-tree node into a single value.
|
||||
local function reduce_template_expression(tree)
|
||||
if tree.op == "|" then
|
||||
return (reduce_template_expression(tree.lhs) ~= "0" or reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
|
||||
elseif tree.op == "&" then
|
||||
return (reduce_template_expression(tree.lhs) ~= "0" and reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
|
||||
elseif tree.op == ">" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) > (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
|
||||
elseif tree.op == ">=" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) >= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
|
||||
elseif tree.op == "<" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) < (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
|
||||
elseif tree.op == "<=" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) <= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
|
||||
elseif tree.op == "=" or tree.op == "==" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) == (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
|
||||
elseif tree.op == "+" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) + (tonumber(reduce_template_expression(tree.rhs)) or 0)
|
||||
elseif tree.op == "*" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) * (tonumber(reduce_template_expression(tree.rhs)) or 0)
|
||||
elseif tree.op == "/" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) / (tonumber(reduce_template_expression(tree.rhs)) or 0)
|
||||
elseif tree.op == "%" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) % (tonumber(reduce_template_expression(tree.rhs)) or 0)
|
||||
elseif tree.op == "^" then
|
||||
return (tonumber(reduce_template_expression(tree.lhs)) or 0) ^ (tonumber(reduce_template_expression(tree.rhs)) or 0)
|
||||
else
|
||||
return tree.value
|
||||
end
|
||||
end
|
||||
|
||||
-- Evaluate an interpolation expression, with an optional table of variables.
|
||||
local function evaluate_template_expression(expr, vars, depth)
|
||||
if expr == "" then return "0" end
|
||||
if not depth then depth = 0 end
|
||||
-- This handles the case where vars is omitted, because then it ends up
|
||||
-- just setting vars to an empty table.
|
||||
if type(vars) ~= "table" then
|
||||
vars = {item = vars}
|
||||
end
|
||||
|
||||
-- Check for operators early, because variables may contain punctuation when expanded.
|
||||
-- A class is used instead of %p because %p matches @, which is used for variables.
|
||||
local has_operators = expr:find("[|&><=+*/%%^]")
|
||||
|
||||
-- Expand all variables so we can deal with a constexpr.
|
||||
local offset = 1
|
||||
while offset < 100000 do
|
||||
local a, b, name = expr:find("@([%a_]+)", offset)
|
||||
if not a then break end
|
||||
-- If referencing an undefined variable, default to 0 because it's safest that way.
|
||||
local result = minetest.formspec_escape(tostring(vars[name] or "0"))
|
||||
expr = expr:sub(1, a -1)..result..expr:sub(b +1)
|
||||
offset = a +#result
|
||||
end
|
||||
|
||||
-- If there are no operators, this is a constant expression and we can just return.
|
||||
if not has_operators then return expr end
|
||||
|
||||
-- Condense sub-expressions.
|
||||
local offset = 1
|
||||
while offset < 100000 do
|
||||
local a, b, se = expr:find("(%b())", offset)
|
||||
if not a then break end
|
||||
se = se:gsub("^%((.*)%)$", "%1")
|
||||
local result = evaluate_template_expression(se, vars, depth +1)
|
||||
expr = expr:sub(1, a -1)..result..expr:sub(b +1)
|
||||
offset = a +#result
|
||||
end
|
||||
|
||||
-- Expression parsing
|
||||
local tree = split_template_expression(expr)
|
||||
return tostring(reduce_template_expression(tree))
|
||||
end
|
||||
|
||||
-- Do variable interpolation for the given formspec using the provided variable table.
|
||||
local function evaluate_template_block(fs, vars)
|
||||
local offset = 0
|
||||
while offset < 100000 do
|
||||
local s_start, s_end, s_name, s_expr = fs:find("@set:@?([%w_]+):([^\n]+)", offset)
|
||||
local i_start, i_end, i_expr = fs:find("%${([^}]-)}", offset)
|
||||
if not s_start and not i_start then break end
|
||||
if s_start and s_start < (i_start or math.huge) then
|
||||
-- Assignment statements
|
||||
vars[s_name] = evaluate_template_expression(s_expr, vars)
|
||||
fs = fs:sub(1, s_start -1)..fs:sub(s_end +1)
|
||||
offset = s_start
|
||||
elseif i_start then
|
||||
-- Interpolations
|
||||
local result = evaluate_template_expression(i_expr, vars)
|
||||
fs = fs:sub(1, i_start -1)..result..fs:sub(i_end +1)
|
||||
offset = i_start +#result
|
||||
end
|
||||
end
|
||||
return fs
|
||||
end
|
||||
|
||||
-- Interpret the list expression of a foreach loop, then iterate.
|
||||
local function evaluate_template_foreach(loop, vars)
|
||||
local out = ""
|
||||
local list = {}
|
||||
if loop.foreach:sub(1,1) == "[" then
|
||||
list = loop.foreach:gsub("^%[(.*)%]$", "%1"):split(",")
|
||||
elseif loop.foreach:sub(1, 1) == "@" then
|
||||
local var = loop.foreach:sub(2)
|
||||
-- Builtin variables take precedence.
|
||||
if var == "WORLDS" then
|
||||
list = get_worlds_for_game(state.current_game.id)
|
||||
elseif var == "MODS" then
|
||||
list = get_mods_for_game(state.current_game.id)
|
||||
elseif var == "WORLDMODS" then
|
||||
list = state.menu_vars.selected_world and get_mods_for_world(state.menu_vars.selected_world) or {}
|
||||
else
|
||||
list = vars[var]
|
||||
end
|
||||
end
|
||||
for i, x in ipairs(list) do
|
||||
local vars2 = {}
|
||||
if type(x) ~= "table" then
|
||||
vars2.item = x
|
||||
else
|
||||
for k, v in pairs(x) do
|
||||
vars2[k] = v
|
||||
end
|
||||
end
|
||||
vars.i = i
|
||||
out = out..evaluate_game_dialog(loop.content, setmetatable(vars2, {__index = vars, __newindex = vars})).."\n"
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Determine which branch of an if statement should be evaluated.
|
||||
local function evaluate_template_conditional(cond, vars)
|
||||
local out = ""
|
||||
local list = {}
|
||||
local condition = evaluate_template_expression(cond.condition, vars)
|
||||
if condition ~= "0" then
|
||||
out = out..evaluate_game_dialog(cond.content, vars).."\n"
|
||||
else
|
||||
out = out..evaluate_game_dialog(cond.else_content, vars).."\n"
|
||||
end
|
||||
-- print("Evaluated condition `"..cond.condition.."` as "..out)
|
||||
return out
|
||||
end
|
||||
|
||||
-- Process the syntax tree for a game dialog and output the resulting string.
|
||||
function evaluate_game_dialog(dialog, vars)
|
||||
local out = ""
|
||||
if not dialog then return out end
|
||||
if type(dialog) == "string" then
|
||||
return evaluate_template_block(dialog, vars)
|
||||
elseif dialog.condition then
|
||||
out = out..evaluate_template_conditional(dialog, vars)
|
||||
elseif dialog.foreach then
|
||||
out = out..evaluate_template_foreach(dialog, vars)
|
||||
else
|
||||
for _, c in ipairs(dialog) do
|
||||
out = out..evaluate_game_dialog(c, vars)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
template = {}
|
||||
|
||||
function template.build_expr(expr)
|
||||
return {type = "expr", value = expr}
|
||||
end
|
||||
|
||||
function template.build(menu)
|
||||
-- First pass: Collect all tokens into a flat list
|
||||
local block = {}
|
||||
local item
|
||||
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 stmt_start < (expr_start or math.huge) and stmt_start < (comment_start or math.huge) then
|
||||
block[#block +1] = menu:sub(offset, stmt_start -1)
|
||||
|
||||
local verb = stmt:match "^(%a+)"
|
||||
if verb == "if" then
|
||||
item = {
|
||||
type = "if",
|
||||
parent = block,
|
||||
conditions = {{condition = template.build_expr(stmt:match "if%s*(.*)"), body = {}}}
|
||||
}
|
||||
block[#block +1] = item
|
||||
block = item.conditions[1].body
|
||||
elseif verb == "elseif" then
|
||||
local x = {
|
||||
condition = template.build_expr(stmt:match "if%s*(.*)"),
|
||||
body = {}
|
||||
}
|
||||
item.conditions[#item.conditions +1] = x
|
||||
block = x.body
|
||||
elseif verb == "else" then
|
||||
local x = {
|
||||
condition = template.build_expr("true"),
|
||||
body = {}
|
||||
}
|
||||
item.conditions[#item.conditions +1] = x
|
||||
block = x.body
|
||||
elseif verb == "endif" then
|
||||
block = item.parent
|
||||
elseif verb == "for" then
|
||||
|
||||
elseif verb == "endfor" then
|
||||
|
||||
elseif verb == "macro" then
|
||||
|
||||
elseif verb == "endmacro" then
|
||||
|
||||
elseif verb == "set" then
|
||||
|
||||
elseif verb == "endset" then
|
||||
|
||||
elseif verb == "include" then
|
||||
|
||||
end
|
||||
print(verb)
|
||||
|
||||
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
|
||||
offset = expr_end
|
||||
pos = expr_end
|
||||
matched = true
|
||||
end
|
||||
|
||||
if not matched then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return block
|
||||
end
|
||||
|
||||
function template.evaluate_expr(expr)
|
||||
if expr.type == "" then
|
||||
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function template.evaluate(dialog, vars)
|
||||
local out = ""
|
||||
for _, item in ipairs(dialog) do
|
||||
if type(item) == "string" then
|
||||
out = out..item
|
||||
else
|
||||
if item.type == "if" then
|
||||
for _, cond in ipairs(item.conditions) do
|
||||
if template.evaluate_expr(cond.condition) then
|
||||
out = out..template.evaluate(cond.body)
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif item.type == "for" then
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
print(template.evaluate(template.build [[
|
||||
This is a {{blah}}.
|
||||
|
||||
{% if foo %}
|
||||
Foo
|
||||
{% elseif bar %}
|
||||
Bar
|
||||
{% else %}
|
||||
Baz
|
||||
{% endif %}
|
||||
|
||||
{% for x in [a,b,c] %}
|
||||
The item is {{ x }}!
|
||||
Uppercase: {{ x:upper() }}
|
||||
{% endfor %}
|
||||
]], {
|
||||
foo = false
|
||||
}))
|
||||
Loading…
Add table
Add a link
Reference in a new issue