-- -- 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)