248 lines
7.6 KiB
Lua
248 lines
7.6 KiB
Lua
--
|
|
-- formspec_ast: An abstract syntax tree for formspecs.
|
|
--
|
|
-- This does not actually depend on Minetest and could probably run in
|
|
-- standalone Lua.
|
|
--
|
|
-- The MIT License (MIT)
|
|
--
|
|
-- Copyright © 2019-2022 by luk3yx.
|
|
--
|
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
-- of this software and associated documentation files (the "Software"), to
|
|
-- deal in the Software without restriction, including without limitation the
|
|
-- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
-- sell copies of the Software, and to permit persons to whom the Software is
|
|
-- furnished to do so, subject to the following conditions:
|
|
--
|
|
-- The above copyright notice and this permission notice shall be included in
|
|
-- all copies or substantial portions of the Software.
|
|
--
|
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
-- IN THE SOFTWARE.
|
|
--
|
|
|
|
local formspec_ast, minetest = formspec_ast, formspec_ast.minetest
|
|
|
|
-- Expose minetest.formspec_escape for use outside of Minetest
|
|
formspec_ast.formspec_escape = minetest.formspec_escape
|
|
|
|
-- Parses and unparses plain formspecs and just unparses AST trees.
|
|
function formspec_ast.interpret(spec, custom_handlers)
|
|
local ast = spec
|
|
if type(spec) == 'string' then
|
|
local err
|
|
ast, err = formspec_ast.parse(spec, custom_handlers)
|
|
if not ast then
|
|
return nil, err
|
|
end
|
|
end
|
|
return formspec_ast.unparse(ast)
|
|
end
|
|
|
|
local function walk_inner(tree, container_elems)
|
|
-- Use two tables to store values so that a new table doesn't have to be
|
|
-- created every time a container is entered
|
|
local parent_trees = {}
|
|
local parent_indexes = {}
|
|
|
|
local parent_idx = 0
|
|
local i = 0
|
|
return function()
|
|
-- If the previously yielded element has children
|
|
if i > 0 and container_elems[tree[i].type] then
|
|
-- Save the parent element and the next index
|
|
parent_idx = parent_idx + 1
|
|
parent_trees[parent_idx] = tree
|
|
parent_indexes[parent_idx] = i + 1
|
|
|
|
-- Set the new tree
|
|
tree = tree[i]
|
|
|
|
-- Reset I to initial value (zero)
|
|
i = 0
|
|
end
|
|
|
|
-- Point index to next child
|
|
i = i + 1
|
|
|
|
-- Get child at index
|
|
local elem = tree[i]
|
|
while not elem do -- current child is invalid
|
|
if parent_idx < 1 then
|
|
return
|
|
end
|
|
|
|
-- Restore parent's relative index
|
|
tree, i = parent_trees[parent_idx], parent_indexes[parent_idx]
|
|
parent_idx = parent_idx - 1
|
|
|
|
-- Get child at index
|
|
elem = tree[i]
|
|
end
|
|
|
|
return elem, tree, i
|
|
end
|
|
end
|
|
|
|
-- Returns an iterator over all nodes in a formspec AST, including ones in
|
|
-- containers.
|
|
local default_container_elems = {container = true, scroll_container = true}
|
|
function formspec_ast.walk(tree, provided_container_elms)
|
|
return walk_inner(tree, provided_container_elms or default_container_elems)
|
|
end
|
|
|
|
-- Similar to formspec_ast.walk(), however only returns nodes which have a type
|
|
-- of `node_type`.
|
|
function formspec_ast.find(tree, node_type)
|
|
local walk = formspec_ast.walk(tree)
|
|
return function()
|
|
local node
|
|
repeat
|
|
node = walk()
|
|
until node == nil or node.type == node_type
|
|
return node
|
|
end
|
|
end
|
|
|
|
-- Returns the first element in the AST tree that has the given name.
|
|
function formspec_ast.get_element_by_name(tree, name)
|
|
for elem in formspec_ast.walk(tree) do
|
|
if elem.name == name then
|
|
return elem
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Returns a table/list/array of all elements in the AST tree that have the
|
|
-- given name.
|
|
function formspec_ast.get_elements_by_name(tree, name)
|
|
local res = {}
|
|
for elem in formspec_ast.walk(tree) do
|
|
if elem.name == name then
|
|
table.insert(res, elem)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Offsets all elements in an element list.
|
|
function formspec_ast.apply_offset(elems, x, y)
|
|
x, y = x or 0, y or 0
|
|
for _, elem in ipairs(elems) do
|
|
if type(elem.x) == 'number' and type(elem.y) == 'number' then
|
|
elem.x = elem.x + x
|
|
elem.y = elem.y + y
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Removes container elements and fixes nodes inside containers.
|
|
local flatten_containers = {container = true}
|
|
function formspec_ast.flatten(tree)
|
|
local res = {formspec_version=tree.formspec_version}
|
|
for elem in walk_inner(table.copy(tree), flatten_containers) do
|
|
if elem.type == 'container' then
|
|
formspec_ast.apply_offset(elem, elem.x, elem.y)
|
|
else
|
|
table.insert(res, elem)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Similar to minetest.show_formspec, however is passed through
|
|
-- formspec_ast.interpret first and will return an error message if the
|
|
-- formspec could not be parsed.
|
|
function formspec_ast.show_formspec(player, formname, formspec)
|
|
if minetest.is_player(player) then
|
|
player = player:get_player_name()
|
|
end
|
|
if type(player) ~= 'string' or player == '' then
|
|
return 'No such player!'
|
|
end
|
|
|
|
local new_fs, err = formspec_ast.interpret(formspec)
|
|
if new_fs then
|
|
minetest.show_formspec(player, formname, new_fs)
|
|
else
|
|
minetest.log('warning', 'formspec_ast.show_formspec(): ' ..
|
|
tostring(err))
|
|
return err
|
|
end
|
|
end
|
|
|
|
-- Alias invsize[] to size[]
|
|
formspec_ast.register_element('invsize', function(raw, parse)
|
|
return {
|
|
type = 'size',
|
|
w = parse.number(raw[1][1]),
|
|
h = parse.number(raw[1][2]),
|
|
}
|
|
end)
|
|
|
|
-- Centered labels
|
|
-- Credit to https://github.com/v-rob/minetest_formspec_game for the click
|
|
-- animation workaround.
|
|
-- This may be removed from a later formspec_ast release.
|
|
-- size[5,2]formspec_ast:centered_label[0,0;5,1;Centered label]
|
|
formspec_ast.register_element('formspec_ast:centered_label', function(raw,
|
|
parse)
|
|
-- Create a container
|
|
return {
|
|
type = 'container',
|
|
x = parse.number(raw[1][1]),
|
|
y = parse.number(raw[1][2]),
|
|
|
|
-- Add a background-less image button with the text.
|
|
{
|
|
type = 'image_button',
|
|
x = 0,
|
|
y = 0,
|
|
w = parse.number(raw[2][1]),
|
|
h = parse.number(raw[2][2]),
|
|
texture_name = 'blank.png',
|
|
name = '',
|
|
label = parse.string(raw[3]),
|
|
noclip = true,
|
|
drawborder = false,
|
|
pressed_texture_name = '',
|
|
},
|
|
|
|
-- Add another background-less image button to hack around the click
|
|
-- animation.
|
|
{
|
|
type = 'image_button',
|
|
x = 0,
|
|
y = 0,
|
|
w = parse.number(raw[2][1]),
|
|
h = parse.number(raw[2][2]),
|
|
texture_name = '',
|
|
name = '',
|
|
label = '',
|
|
noclip = true,
|
|
drawborder = false,
|
|
pressed_texture_name = '',
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- Add a formspec element to crash clients
|
|
-- This may be removed from a later formspec_ast release.
|
|
formspec_ast.register_element('formspec_ast:crash', function(_, _)
|
|
return {
|
|
type = 'list',
|
|
inventory_location = '___die',
|
|
list_name = 'crash',
|
|
x = 0,
|
|
y = 0,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
end)
|