formspec_ast/helpers.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)