Initial commit
This commit is contained in:
commit
b2399d6825
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
test.lua
|
22
LICENSE.md
Normal file
22
LICENSE.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2019 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.
|
182
README.md
Normal file
182
README.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# formspec_ast
|
||||||
|
|
||||||
|
A Minetest mod library to make modifying formspecs easier.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- `formspec_ast.parse(formspec_string)`: Parses `formspec_string` and returns
|
||||||
|
an AST.
|
||||||
|
- `formspec_ast.unparse(tree)`: Unparses the abstract syntax tree provided
|
||||||
|
and returns a formspec string.
|
||||||
|
- `formspec_ast.register_element('modname:element', function())`: Registers
|
||||||
|
a custom element. See [registering elements] for more information.
|
||||||
|
- `formspec_ast.interpret(string_or_tree)`: Returns a formspec string after
|
||||||
|
(optionally parsing) and unparsing the formspec provided.
|
||||||
|
- `formspec_ast.walk(tree)`: Returns an iterator (use this directly in a for
|
||||||
|
loop) that will return all nodes in a tree, including ones in side
|
||||||
|
containers.
|
||||||
|
- `formspec_ast.find(tree, node_type)`: Similar to `walk(tree)`, however only
|
||||||
|
returns `node_type` nodes.
|
||||||
|
- `formspec_ast.get_element_by_name(tree, name)`: Returns the first element in
|
||||||
|
the tree with the name `name`.
|
||||||
|
- `formspec_ast.get_elements_by_name(tree, name)`: Returns a list of all the
|
||||||
|
elements with the name `name`.
|
||||||
|
- `formspec_ast.apply_offset(tree, x, y)`: Shifts all elements in `tree`.
|
||||||
|
Similar to `container`.
|
||||||
|
- `formspec_ast.flatten(tree)`: Removes all containers and offsets elements
|
||||||
|
that were in containers accordingly.
|
||||||
|
- `formspec_ast.show_formspec(player_or_name, formname, formspec)`: Similar
|
||||||
|
to `minetest.show_formspec`, however also accepts player objects and will
|
||||||
|
pass `formspec` through `formspec_ast.interpret` first.
|
||||||
|
- `formspec_ast.safe_parse(string_or_tree)`: Similar to `formspec_ast.parse`,
|
||||||
|
however will delete any elements that may crash the client (or any I
|
||||||
|
haven't added to the whitelist).
|
||||||
|
|
||||||
|
## AST
|
||||||
|
|
||||||
|
The AST is similar (and generated from) [the formspec element list], however
|
||||||
|
all attributes are lowercase.
|
||||||
|
|
||||||
|
[the formspec element list]: https://github.com/minetest/minetest/blob/dee2210/doc/lua_api.txt#L1959
|
||||||
|
|
||||||
|
### Special cases
|
||||||
|
|
||||||
|
- `formspec_version` (provided it is the first element) is moved to
|
||||||
|
`tree.formspec_version` (`1` by default).
|
||||||
|
|
||||||
|
### `formspec_ast.parse` example
|
||||||
|
|
||||||
|
*Note that the whitespace in the formspec is optional and exists for
|
||||||
|
readability. Non-numeric table items in the `dump()` output are re-ordered for
|
||||||
|
readability.*
|
||||||
|
|
||||||
|
```lua
|
||||||
|
> tree = formspec_ast.parse('size[5,2] '
|
||||||
|
>> .. 'style[name;bgcolor=blue;textcolor=yellow]'
|
||||||
|
>> .. 'container[1,1]'
|
||||||
|
>> .. ' label[0,0;Containers are fun]'
|
||||||
|
>> .. ' container[-1,-1]'
|
||||||
|
>> .. ' button[0.5,0;4,1;name;Label]'
|
||||||
|
>> .. ' container_end[]'
|
||||||
|
>> .. ' label[0,1;Nested containers work too.]'
|
||||||
|
>> .. 'container_end[]'
|
||||||
|
>> .. ' image[0,1;1,1;air.png]')
|
||||||
|
> print(dump(tree))
|
||||||
|
{
|
||||||
|
formspec_version = 1,
|
||||||
|
{
|
||||||
|
type = "size",
|
||||||
|
w = 5,
|
||||||
|
h = 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "style",
|
||||||
|
name = "name",
|
||||||
|
props = {
|
||||||
|
bgcolor = "blue",
|
||||||
|
textcolor = "yellow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "container",
|
||||||
|
x = 1,
|
||||||
|
y = 1,
|
||||||
|
{
|
||||||
|
type = "label",
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
label = "Containers are fun",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "container",
|
||||||
|
x = -1,
|
||||||
|
y = -1,
|
||||||
|
{
|
||||||
|
type = "button",
|
||||||
|
x = 0.5,
|
||||||
|
y = 0,
|
||||||
|
w = 4,
|
||||||
|
h = 1,
|
||||||
|
name = "name",
|
||||||
|
label = "Label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "label",
|
||||||
|
x = 0,
|
||||||
|
y = 1,
|
||||||
|
label = "Nested containers work too.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "image",
|
||||||
|
x = 0,
|
||||||
|
y = 1,
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
texture_name = "air.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `formspec_ast.flatten` example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
> print(dump(formspec_ast.flatten(tree)))
|
||||||
|
{
|
||||||
|
formspec_version = 1,
|
||||||
|
{
|
||||||
|
type = "size",
|
||||||
|
w = 5,
|
||||||
|
h = 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "style",
|
||||||
|
name = "name",
|
||||||
|
props = {
|
||||||
|
bgcolor = "blue",
|
||||||
|
textcolor = "yellow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "label",
|
||||||
|
x = 1,
|
||||||
|
y = 1,
|
||||||
|
label = "Containers are fun",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "button",
|
||||||
|
x = 0.5,
|
||||||
|
y = 0,
|
||||||
|
w = 4
|
||||||
|
h = 1,
|
||||||
|
name = "name",
|
||||||
|
label = "Label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "label",
|
||||||
|
x = 1,
|
||||||
|
y = 2,
|
||||||
|
label = "Nested containers work too.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "image",
|
||||||
|
x = 0,
|
||||||
|
y = 1,
|
||||||
|
w = 1,
|
||||||
|
h = 1,
|
||||||
|
texture_name = "air.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `formspec_ast.unparse` example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
> print(formspec_ast.unparse(tree))
|
||||||
|
size[5,2,]style[name;textcolor=yellow;bgcolor=blue]container[1,1]label[0,0;Containers are fun]container[-1,-1]button[0.5,0;4,1;name;Label]container_end[]label[0,1;Nested containers work too.]container_end[]image[0,1;1,1;air.png]
|
||||||
|
|
||||||
|
> print(formspec_ast.unparse(formspec_ast.flatten(tree)))
|
||||||
|
size[5,2,]style[name;textcolor=yellow;bgcolor=blue]label[1,1;Containers are fun]button[0.5,0;4,1;name;Label]label[1,2;Nested containers work too.]image[0,1;1,1;air.png]
|
||||||
|
```
|
548
core.lua
Normal file
548
core.lua
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
--
|
||||||
|
-- formspec_ast: An abstract system tree for formspecs.
|
||||||
|
--
|
||||||
|
-- This does not actually depend on Minetest and could probably run in
|
||||||
|
-- standalone Lua.
|
||||||
|
--
|
||||||
|
-- The MIT License (MIT)
|
||||||
|
--
|
||||||
|
-- Copyright © 2019 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
|
||||||
|
|
||||||
|
-- Parse a formspec into a "raw" non-AST state.
|
||||||
|
-- Input: size[5,2]button[0,0;5,1;name;Label] image[0,1;1,1;air.png]
|
||||||
|
-- Output: {{'size', '5', '2'}, {'button', '0,0', '5,1', 'name', 'label'},
|
||||||
|
-- {'image', '0,1', '1,1', 'air.png'}}
|
||||||
|
local function raw_parse(spec)
|
||||||
|
local res = {}
|
||||||
|
while spec do
|
||||||
|
-- Get the first element
|
||||||
|
local name
|
||||||
|
name, spec = spec:match('([%w_%-:]*[^%s\\])%s*(%[.*)')
|
||||||
|
if not name or not spec then return res end
|
||||||
|
local elem = {}
|
||||||
|
elem[1] = name
|
||||||
|
|
||||||
|
-- Get the parameters
|
||||||
|
local s, e = spec:find('[^\\]%]')
|
||||||
|
local rawargs
|
||||||
|
if s and e then
|
||||||
|
rawargs, spec = spec:sub(2, s), spec:sub(e + 1)
|
||||||
|
else
|
||||||
|
rawargs, spec = spec:sub(2), false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Split everything
|
||||||
|
-- TODO: Make this a RegEx
|
||||||
|
local i = ''
|
||||||
|
local esc = false
|
||||||
|
local inner = {}
|
||||||
|
for c = 1, #rawargs do
|
||||||
|
local char = rawargs:sub(c, c)
|
||||||
|
if esc then
|
||||||
|
esc = false
|
||||||
|
i = i .. char
|
||||||
|
elseif char == '\\' then
|
||||||
|
esc = true
|
||||||
|
elseif char == ';' then
|
||||||
|
if #inner > 0 then
|
||||||
|
table.insert(inner, i)
|
||||||
|
table.insert(elem, inner)
|
||||||
|
inner = {}
|
||||||
|
else
|
||||||
|
table.insert(elem, i)
|
||||||
|
end
|
||||||
|
i = ''
|
||||||
|
elseif char == ',' then
|
||||||
|
table.insert(inner, i)
|
||||||
|
i = ''
|
||||||
|
else
|
||||||
|
i = i .. char
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if i ~= '' or #elem > 1 then
|
||||||
|
if #inner > 0 then
|
||||||
|
table.insert(inner, i)
|
||||||
|
table.insert(elem, inner)
|
||||||
|
inner = {}
|
||||||
|
else
|
||||||
|
table.insert(elem, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(res, elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Unparse raw formspecs
|
||||||
|
-- WARNING: This will modify the table passed to it.
|
||||||
|
local function raw_unparse(data)
|
||||||
|
local res = ''
|
||||||
|
for _, elem in ipairs(data) do
|
||||||
|
for i = 2, #elem do
|
||||||
|
if type(elem[i]) == 'table' then
|
||||||
|
for j, e in ipairs(elem[i]) do
|
||||||
|
elem[i][j] = minetest.formspec_escape(e)
|
||||||
|
end
|
||||||
|
elem[i] = table.concat(elem[i], ',')
|
||||||
|
else
|
||||||
|
elem[i] = minetest.formspec_escape(elem[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
res = res .. table.remove(elem, 1) .. '[' ..
|
||||||
|
table.concat(elem, ';') .. ']'
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Elements
|
||||||
|
-- The element format is currently not intuitive.
|
||||||
|
local elements = assert(loadfile(formspec_ast.modpath .. '/elements.lua'))()
|
||||||
|
|
||||||
|
-- Parsing
|
||||||
|
local types = {}
|
||||||
|
|
||||||
|
function types.null() end
|
||||||
|
function types.undefined()
|
||||||
|
error('Unknown element type!')
|
||||||
|
end
|
||||||
|
|
||||||
|
function types.string(str)
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
function types.number(raw_num)
|
||||||
|
local num = tonumber(raw_num)
|
||||||
|
assert(num and num == num, 'Invalid number: "' .. raw_num .. '".')
|
||||||
|
return num
|
||||||
|
end
|
||||||
|
|
||||||
|
function types.boolean(bool)
|
||||||
|
if bool ~= '' then
|
||||||
|
return minetest.is_yes(bool)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function types.table(obj)
|
||||||
|
local s, e = obj:find('=', nil, true)
|
||||||
|
assert(s, 'Invalid syntax: "' .. obj .. '".')
|
||||||
|
return {[obj:sub(1, s - 1)] = obj:sub(e + 1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
function types.null(null)
|
||||||
|
assert(null:trim() == '', 'No value expected!')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_value(elems, template)
|
||||||
|
local elems_l, template_l = #elems, #template
|
||||||
|
if elems_l < template_l or (elems_l > template_l and
|
||||||
|
template[template_l][2] ~= '...') then
|
||||||
|
while #elems > #template and elems[#elems]:trim() == '' do
|
||||||
|
elems[#elems] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(#elems == #template, 'Bad element length.')
|
||||||
|
end
|
||||||
|
|
||||||
|
local res = {}
|
||||||
|
if elems.type then res.type = elems.type end
|
||||||
|
for i, obj in ipairs(template) do
|
||||||
|
local val
|
||||||
|
if obj[2] == '...' then
|
||||||
|
assert(template[i + 1] == nil, 'Invalid template!')
|
||||||
|
|
||||||
|
local elems2 = {}
|
||||||
|
for j = i, #elems do
|
||||||
|
table.insert(elems2, elems[j])
|
||||||
|
end
|
||||||
|
types['...'](elems2, obj[1], res)
|
||||||
|
elseif type(obj[2]) == 'string' then
|
||||||
|
local func = types[obj[2]] or types.undefined
|
||||||
|
local elem = elems[i]
|
||||||
|
if type(elem) == 'table' then
|
||||||
|
elem = table.concat(elem, ',')
|
||||||
|
end
|
||||||
|
res[obj[1]] = func(elem, obj[1])
|
||||||
|
else
|
||||||
|
local elem = elems[i]
|
||||||
|
if type(elem) == 'string' then
|
||||||
|
elem = {elem}
|
||||||
|
end
|
||||||
|
while #obj > #elem do
|
||||||
|
table.insert(elem, '')
|
||||||
|
end
|
||||||
|
val = parse_value(elem, obj)
|
||||||
|
for k, v in pairs(val) do
|
||||||
|
res[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
types['...'] = function(elems, obj, res)
|
||||||
|
local template = {obj}
|
||||||
|
local val = {}
|
||||||
|
local is_string = type(obj[2]) == 'string'
|
||||||
|
for i, elem in ipairs(elems) do
|
||||||
|
local n = parse_value({elem}, template)
|
||||||
|
if is_string then
|
||||||
|
n = n[obj[1]]
|
||||||
|
end
|
||||||
|
table.insert(val, n)
|
||||||
|
end
|
||||||
|
|
||||||
|
if obj[2] == 'table' then
|
||||||
|
local t = {}
|
||||||
|
for _, n in ipairs(val) do
|
||||||
|
local k, v = next(n)
|
||||||
|
t[k] = v
|
||||||
|
end
|
||||||
|
res[obj[1] .. 's'] = t
|
||||||
|
elseif type(obj[2]) == 'string' then
|
||||||
|
res[obj[1]] = val
|
||||||
|
else
|
||||||
|
assert(type(val) == 'table')
|
||||||
|
res[res.type or 'data'] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local parse_mt
|
||||||
|
local function parse_elem(elem, custom_handlers)
|
||||||
|
elem.type = table.remove(elem, 1)
|
||||||
|
local data = elements[elem.type]
|
||||||
|
if not data then
|
||||||
|
if not custom_handlers or not custom_handlers[elem.type] then
|
||||||
|
return false, 'Unknown element "' .. tostring(elem.type) .. '".'
|
||||||
|
end
|
||||||
|
local parse = {}
|
||||||
|
setmetatable(parse, parse_mt)
|
||||||
|
local good, ast_elem = pcall(custom_handlers[elem.type], elem, parse)
|
||||||
|
if good and (not ast_elem or not ast_elem.type) then
|
||||||
|
good, ast_elem = false, "Function didn't return AST element!"
|
||||||
|
end
|
||||||
|
if good then
|
||||||
|
return ast_elem
|
||||||
|
else
|
||||||
|
table.insert(elem, 1, elem.type)
|
||||||
|
return nil, 'Invalid element "' .. raw_unparse({elem}) .. '": ' ..
|
||||||
|
tostring(ast_elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local good, ast_elem
|
||||||
|
for i, template in ipairs(data) do
|
||||||
|
if type(template) == 'function' then
|
||||||
|
good, ast_elem = pcall(template, elem)
|
||||||
|
if good and (not ast_elem or not ast_elem.type) then
|
||||||
|
good, ast_elem = false, "Function didn't return AST element!"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
good, ast_elem = pcall(parse_value, elem, template)
|
||||||
|
end
|
||||||
|
if good then
|
||||||
|
return ast_elem
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(elem, 1, elem.type)
|
||||||
|
return nil, 'Invalid element "' .. raw_unparse({elem}) .. '": ' ..
|
||||||
|
tostring(ast_elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse a formspec into a formspec AST.
|
||||||
|
-- Input: size[5,2] style[name;bgcolor=blue;textcolor=yellow]
|
||||||
|
-- button[0,0;5,1;name;Label] image[0,1;1,1;air.png]
|
||||||
|
-- Output:
|
||||||
|
-- {
|
||||||
|
-- formspec_version = 1,
|
||||||
|
-- {
|
||||||
|
-- type = "size",
|
||||||
|
-- w = 5,
|
||||||
|
-- h = 2,
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- type = "style",
|
||||||
|
-- name = "name",
|
||||||
|
-- props = {
|
||||||
|
-- bgcolor = "blue",
|
||||||
|
-- textcolor = "yellow",
|
||||||
|
-- },
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- type = "button",
|
||||||
|
-- x = 0,
|
||||||
|
-- y = 0,
|
||||||
|
-- w = 5,
|
||||||
|
-- h = 1,
|
||||||
|
-- name = "name",
|
||||||
|
-- label = "Label",
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- type = "image",
|
||||||
|
-- x = 0,
|
||||||
|
-- y = 1,
|
||||||
|
-- w = 1,
|
||||||
|
-- h = 1,
|
||||||
|
-- texture_name = "air.png",
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
|
||||||
|
|
||||||
|
function formspec_ast.parse(spec, custom_handlers)
|
||||||
|
spec = raw_parse(spec)
|
||||||
|
local res = {formspec_version=1}
|
||||||
|
local containers = {}
|
||||||
|
local container = res
|
||||||
|
for _, elem in ipairs(spec) do
|
||||||
|
local ast_elem, err = parse_elem(elem, custom_handlers)
|
||||||
|
if not ast_elem then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
table.insert(container, ast_elem)
|
||||||
|
if ast_elem.type == 'container' then
|
||||||
|
table.insert(containers, container)
|
||||||
|
container = ast_elem
|
||||||
|
elseif ast_elem.type == 'container_end' then
|
||||||
|
container[#container] = nil
|
||||||
|
container = table.remove(containers)
|
||||||
|
if not container then
|
||||||
|
return nil, 'Mismatched container_end[]!'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if res[1] and res[1].type == 'formspec_version' then
|
||||||
|
res.formspec_version = table.remove(res, 1).version
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Unparsing
|
||||||
|
local function unparse_ellipsis(elem, obj1, res, inner)
|
||||||
|
if obj1[2] == 'table' then
|
||||||
|
local value = elem[obj1[1] .. 's']
|
||||||
|
assert(type(value) == 'table', 'Invalid AST!')
|
||||||
|
for k, v in pairs(value) do
|
||||||
|
table.insert(res, tostring(k) .. '=' .. tostring(v))
|
||||||
|
end
|
||||||
|
elseif type(obj1[2]) == 'string' then
|
||||||
|
local value = elem[obj1[1]]
|
||||||
|
for k, v in ipairs(value) do
|
||||||
|
table.insert(res, tostring(v))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
assert(inner == nil)
|
||||||
|
local data = elem[elem.type or 'data'] or elem
|
||||||
|
for _, elem2 in ipairs(data) do
|
||||||
|
local r = {}
|
||||||
|
for i, obj2 in ipairs(obj1) do
|
||||||
|
if obj2[2] == '...' then
|
||||||
|
unparse_ellipsis(elem2, obj2[1], r, true)
|
||||||
|
elseif type(obj2[2]) == 'string' then
|
||||||
|
table.insert(r, tostring(elem2[obj2[1]]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(res, r)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unparse_value(elem, template)
|
||||||
|
local res = {}
|
||||||
|
for i, obj in ipairs(template) do
|
||||||
|
if obj[2] == '...' then
|
||||||
|
assert(template[i + 1] == nil, 'Invalid template!')
|
||||||
|
unparse_ellipsis(elem, obj[1], res)
|
||||||
|
elseif type(obj[2]) == 'string' then
|
||||||
|
local value = elem[obj[1]]
|
||||||
|
if value == nil then
|
||||||
|
res[i] = ''
|
||||||
|
else
|
||||||
|
res[i] = tostring(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
res[i] = unparse_value(elem, obj)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local compare_blanks
|
||||||
|
do
|
||||||
|
local function get_nonempty(a)
|
||||||
|
local nonempty = 0
|
||||||
|
for _, i in ipairs(a) do
|
||||||
|
if type(i) == 'string' and i ~= '' then
|
||||||
|
nonempty = nonempty + 1
|
||||||
|
elseif type(i) == 'table' then
|
||||||
|
nonempty = nonempty + get_nonempty(i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
a.nonempty = nonempty
|
||||||
|
return nonempty
|
||||||
|
end
|
||||||
|
|
||||||
|
function compare_blanks(a, b)
|
||||||
|
local a_n, b_n = get_nonempty(a), get_nonempty(b)
|
||||||
|
if a_n == b_n then
|
||||||
|
return #a < #b
|
||||||
|
end
|
||||||
|
return a_n >= b_n
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unparse_elem(elem, res, force)
|
||||||
|
if elem.type == 'container' and not force then
|
||||||
|
local err = unparse_elem(elem, res, true)
|
||||||
|
if err then return err end
|
||||||
|
for _, e in ipairs(elem) do
|
||||||
|
local err = unparse_elem(e, res)
|
||||||
|
if err then return err end
|
||||||
|
end
|
||||||
|
return unparse_elem({type='container_end'}, res, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = elements[elem.type]
|
||||||
|
if not data or (not force and elem.type == 'container_end') then
|
||||||
|
return nil, 'Unknown element "' .. tostring(elem.type) .. '".'
|
||||||
|
end
|
||||||
|
|
||||||
|
local good, raw_elem
|
||||||
|
local possible_elems = {}
|
||||||
|
for i, template in ipairs(data) do
|
||||||
|
if type(template) == 'function' then
|
||||||
|
good, raw_elem = false, 'Unknown element.'
|
||||||
|
else
|
||||||
|
good, raw_elem = pcall(unparse_value, elem, template)
|
||||||
|
end
|
||||||
|
if good then
|
||||||
|
table.insert(raw_elem, 1, elem.type)
|
||||||
|
table.insert(possible_elems, raw_elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Use the shortest element format that doesn't lose any information.
|
||||||
|
if good then
|
||||||
|
table.sort(possible_elems, compare_blanks)
|
||||||
|
table.insert(res, possible_elems[1])
|
||||||
|
else
|
||||||
|
return 'Invalid element with type "' .. tostring(elem.type)
|
||||||
|
.. '": ' .. tostring(raw_elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert a formspec AST back into a formspec.
|
||||||
|
-- Input:
|
||||||
|
-- {
|
||||||
|
-- {
|
||||||
|
-- type = "size",
|
||||||
|
-- w = 5,
|
||||||
|
-- h = 2,
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- type = "button",
|
||||||
|
-- x = 0,
|
||||||
|
-- y = 0,
|
||||||
|
-- w = 5,
|
||||||
|
-- h = 1,
|
||||||
|
-- name = "name",
|
||||||
|
-- label = "Label",
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- type = "image",
|
||||||
|
-- x = 0,
|
||||||
|
-- y = 1,
|
||||||
|
-- w = 1,
|
||||||
|
-- h = 1,
|
||||||
|
-- texture_name = "air.png",
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- Output: size[5,2,]button[0,0;5,1;name;Label]image[0,1;1,1;air.png]
|
||||||
|
function formspec_ast.unparse(spec)
|
||||||
|
local raw_spec = {}
|
||||||
|
for _, elem in ipairs(spec) do
|
||||||
|
local err = unparse_elem(elem, raw_spec)
|
||||||
|
if err then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if spec.formspec_version and spec.formspec_version ~= 1 then
|
||||||
|
table.insert(raw_spec, 1, {'formspec_version',
|
||||||
|
tostring(spec.formspec_version)})
|
||||||
|
end
|
||||||
|
return raw_unparse(raw_spec)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Allow other mods to access raw_parse and raw_unparse. Note that these may
|
||||||
|
-- change or be removed at any time.
|
||||||
|
formspec_ast._raw_parse = raw_parse
|
||||||
|
formspec_ast._raw_unparse = raw_unparse
|
||||||
|
|
||||||
|
-- Register custom elements
|
||||||
|
parse_mt = {}
|
||||||
|
function parse_mt:__index(key)
|
||||||
|
if key == '...' then
|
||||||
|
key = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local func = types[key]
|
||||||
|
if func then
|
||||||
|
return function(obj)
|
||||||
|
if type(obj) == 'table' then
|
||||||
|
obj = table.concat(obj, ',')
|
||||||
|
end
|
||||||
|
return func(obj or '')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function(obj)
|
||||||
|
error('Unknown element type: ' .. tostring(key))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register custom formspec elements.
|
||||||
|
-- `parse_func` gets two parameters: `raw_elem` and `parse`. The parse table
|
||||||
|
-- is the same as the types table above, however unknown types raise an error.
|
||||||
|
-- The function should return either a single AST node or a list of multiple
|
||||||
|
-- nodes.
|
||||||
|
-- Multiple functions can be registered for one element.
|
||||||
|
function formspec_ast.register_element(name, parse_func)
|
||||||
|
assert(type(name) == 'string' and type(parse_func) == 'function')
|
||||||
|
if not elements[name] then
|
||||||
|
elements[name] = {}
|
||||||
|
end
|
||||||
|
local parse = {}
|
||||||
|
setmetatable(parse, parse_mt)
|
||||||
|
table.insert(elements[name], function(raw_elem)
|
||||||
|
local res = parse_func(raw_elem, parse)
|
||||||
|
if type(res) == 'table' and not res.type then
|
||||||
|
res.type = 'container'
|
||||||
|
res.x, res.y = 0, 0
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end)
|
||||||
|
end
|
6
elements.lua
Normal file
6
elements.lua
Normal file
File diff suppressed because one or more lines are too long
206
helpers.lua
Normal file
206
helpers.lua
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
--
|
||||||
|
-- formspec_ast: An abstract system tree for formspecs.
|
||||||
|
--
|
||||||
|
-- This does not actually depend on Minetest and could probably run in
|
||||||
|
-- standalone Lua.
|
||||||
|
--
|
||||||
|
-- The MIT License (MIT)
|
||||||
|
--
|
||||||
|
-- Copyright © 2019 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
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- Returns an iterator over all nodes in a formspec AST, including ones in
|
||||||
|
-- containers.
|
||||||
|
function formspec_ast.walk(tree)
|
||||||
|
local parents = {}
|
||||||
|
local i = 1
|
||||||
|
return function()
|
||||||
|
local res = tree[i]
|
||||||
|
while not res do
|
||||||
|
local n = table.remove(parents)
|
||||||
|
if not n then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
tree, i = n[1], n[2]
|
||||||
|
res = tree[i]
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
if res.type == 'container' then
|
||||||
|
table.insert(parents, {tree, i})
|
||||||
|
tree = res
|
||||||
|
i = 1
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
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.
|
||||||
|
function formspec_ast.flatten(tree)
|
||||||
|
local res = {formspec_version=tree.formspec_version}
|
||||||
|
for elem in formspec_ast.walk(table.copy(tree)) 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 formspec, err = formspec_ast.interpret(formspec)
|
||||||
|
if formspec then
|
||||||
|
minetest.show_formspec(player, formname, formspec)
|
||||||
|
else
|
||||||
|
minetest.log('warning', 'formspec_ast.show_formspec(): ' ..
|
||||||
|
tostring(err))
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Centered labels
|
||||||
|
-- Credit to https://github.com/v-rob/minetest_formspec_game for the click
|
||||||
|
-- animation workaround.
|
||||||
|
-- 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 = '',
|
||||||
|
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
|
||||||
|
formspec_ast.register_element('formspec_ast:crash', function(raw, parse)
|
||||||
|
return {
|
||||||
|
type = 'list',
|
||||||
|
inventory_location = '___die',
|
||||||
|
list_name = 'crash',
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
w = 0,
|
||||||
|
h = 0,
|
||||||
|
}
|
||||||
|
end)
|
60
init.lua
Normal file
60
init.lua
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
--
|
||||||
|
-- formspec_ast: An abstract system tree for formspecs.
|
||||||
|
--
|
||||||
|
-- This does not actually depend on Minetest and could probably run in
|
||||||
|
-- standalone Lua.
|
||||||
|
--
|
||||||
|
-- The MIT License (MIT)
|
||||||
|
--
|
||||||
|
-- Copyright © 2019 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.
|
||||||
|
--
|
||||||
|
|
||||||
|
formspec_ast = {}
|
||||||
|
|
||||||
|
local minetest = minetest
|
||||||
|
local modpath
|
||||||
|
|
||||||
|
if minetest then
|
||||||
|
-- Running inside Minetest.
|
||||||
|
modpath = minetest.get_modpath('formspec_ast')
|
||||||
|
is_yes = minetest.is_yes
|
||||||
|
assert(minetest.get_current_modname() == 'formspec_ast',
|
||||||
|
'This mod must be called formspec_ast!')
|
||||||
|
else
|
||||||
|
-- Probably running outside Minetest.
|
||||||
|
modpath = '.'
|
||||||
|
minetest = core or {}
|
||||||
|
function minetest.is_yes(str)
|
||||||
|
str = str:lower()
|
||||||
|
return str == 'true' or str == 'yes'
|
||||||
|
end
|
||||||
|
function string.trim(str)
|
||||||
|
return str:gsub("^%s*(.-)%s*$", "%1")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
formspec_ast.modpath, formspec_ast.minetest = modpath, minetest
|
||||||
|
|
||||||
|
dofile(modpath .. '/core.lua')
|
||||||
|
dofile(modpath .. '/helpers.lua')
|
||||||
|
dofile(modpath .. '/safety.lua')
|
||||||
|
|
||||||
|
formspec_ast.modpath, formspec_ast.minetest = nil, nil
|
104
lua_dump.py
Normal file
104
lua_dump.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Converts Python objects into Lua ones. This allows more things to be used as
|
||||||
|
# keys than JSON.
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright © 2019 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
def _escape_string(x):
|
||||||
|
yield '"'
|
||||||
|
for char in x:
|
||||||
|
if char == 0x22: # "
|
||||||
|
yield '\"'
|
||||||
|
elif char == 0x5c:
|
||||||
|
yield r'\\'
|
||||||
|
elif 0x7f > char > 0x19:
|
||||||
|
yield chr(char)
|
||||||
|
else:
|
||||||
|
yield '\\' + str(char).zfill(3)
|
||||||
|
yield '"'
|
||||||
|
|
||||||
|
class _PartialTypeError(TypeError):
|
||||||
|
def __str__(self):
|
||||||
|
return 'Object of type ' + repr(type(self.args[0]).__name__) + \
|
||||||
|
' is not Lua serializable.'
|
||||||
|
|
||||||
|
def _dump(obj):
|
||||||
|
if isinstance(obj, set):
|
||||||
|
obj = dict.fromkeys(obj, True)
|
||||||
|
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
res = []
|
||||||
|
for k, v in obj.items():
|
||||||
|
res.append('[' + _dump(k) + '] = ' + _dump(v))
|
||||||
|
return '{' + ', '.join(res) + '}'
|
||||||
|
|
||||||
|
if isinstance(obj, (tuple, list)):
|
||||||
|
return '{' + ', '.join(map(_dump, obj)) + '}'
|
||||||
|
|
||||||
|
if isinstance(obj, bool):
|
||||||
|
return 'true' if obj else 'false'
|
||||||
|
|
||||||
|
if isinstance(obj, (int, float, Decimal)):
|
||||||
|
return str(obj)
|
||||||
|
|
||||||
|
if isinstance(obj, str):
|
||||||
|
obj = obj.encode('utf-8', 'replace')
|
||||||
|
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return ''.join(_escape_string(obj))
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
return 'nil'
|
||||||
|
|
||||||
|
raise _PartialTypeError(obj)
|
||||||
|
|
||||||
|
def dump(obj):
|
||||||
|
"""
|
||||||
|
Similar to serialize(), however doesn't prepend return.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _dump(obj)
|
||||||
|
except _PartialTypeError as e:
|
||||||
|
msg = str(e)
|
||||||
|
|
||||||
|
# Clean tracebacks
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
def serialize(obj):
|
||||||
|
"""
|
||||||
|
Serialize an object into valid Lua code. This will raise a TypeError if the
|
||||||
|
object cannot be serialized into lua.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return 'return ' + _dump(obj)
|
||||||
|
except _PartialTypeError as e:
|
||||||
|
msg = str(e)
|
||||||
|
|
||||||
|
# Clean tracebacks
|
||||||
|
raise TypeError(msg)
|
157
make_elements.py
Executable file
157
make_elements.py
Executable file
@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# A primitive script to parse lua_api.txt for formspec elements.
|
||||||
|
#
|
||||||
|
|
||||||
|
import copy, lua_dump, os, urllib.request
|
||||||
|
|
||||||
|
def _make_known(**kwargs):
|
||||||
|
known = {}
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
for i in v:
|
||||||
|
known[i] = k
|
||||||
|
return known
|
||||||
|
|
||||||
|
_known = _make_known(
|
||||||
|
number=('x', 'y', 'w', 'h', 'selected_idx', 'version',
|
||||||
|
'starting_item_index'),
|
||||||
|
boolean=('auto_clip', 'fixed_size', 'transparent', 'draw_border', 'bool',
|
||||||
|
'fullscreen', 'noclip', 'drawborder', 'selected'),
|
||||||
|
table=('param', 'opt', 'prop'),
|
||||||
|
null=('',),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_name(n):
|
||||||
|
if not isinstance(n, tuple) or n[1] == '...':
|
||||||
|
return '...'
|
||||||
|
return n[0][:-1].rsplit('_', 1)[0].rstrip('_')
|
||||||
|
|
||||||
|
_aliases = {
|
||||||
|
'type': 'elem_type',
|
||||||
|
'cell': 'cells',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _fix_param(param):
|
||||||
|
if isinstance(param, str):
|
||||||
|
if ',' not in param:
|
||||||
|
param = param.lower().strip().strip('<>').replace(' ', '_')
|
||||||
|
param = _aliases.get(param, param)
|
||||||
|
return (param, _known.get(param, 'string'))
|
||||||
|
param = param.split(',')
|
||||||
|
|
||||||
|
res = []
|
||||||
|
for p in param:
|
||||||
|
if p != '...' or not res:
|
||||||
|
res.append(_fix_param(p))
|
||||||
|
continue
|
||||||
|
|
||||||
|
last = res.pop()
|
||||||
|
# Workaround
|
||||||
|
if res and last and isinstance(last, list) and \
|
||||||
|
last[0][0].endswith('2') and isinstance(res[-1], list) and \
|
||||||
|
res[-1] and res[-1][0][0].endswith('1'):
|
||||||
|
last = res.pop()
|
||||||
|
last[0] = (last[0][0][:-2], last[0][1])
|
||||||
|
|
||||||
|
name = _get_name(last)
|
||||||
|
if name == '...':
|
||||||
|
res.append((last, '...'))
|
||||||
|
else:
|
||||||
|
while res and _get_name(res[-1]) == name:
|
||||||
|
res.pop()
|
||||||
|
res.append((_fix_param(name), '...'))
|
||||||
|
break
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
_hooks = {}
|
||||||
|
def hook(name):
|
||||||
|
def add_hook(func):
|
||||||
|
_hooks[name] = func
|
||||||
|
return func
|
||||||
|
return add_hook
|
||||||
|
|
||||||
|
# Fix background9
|
||||||
|
@hook('background9')
|
||||||
|
def _background9_hook(params):
|
||||||
|
assert params[-1] == ('middle', 'string')
|
||||||
|
params[-1] = param = []
|
||||||
|
param.append(('middle_x', 'number'))
|
||||||
|
yield params
|
||||||
|
param.append(('middle_y', 'number'))
|
||||||
|
yield params
|
||||||
|
param.append(('middle_x2', 'number'))
|
||||||
|
param.append(('middle_y2', 'number'))
|
||||||
|
yield params
|
||||||
|
|
||||||
|
def _raw_parse(data):
|
||||||
|
data = data.split('\nElements\n--------\n', 1)[-1].split('\n----', 1)[0]
|
||||||
|
for line in data.split('\n'):
|
||||||
|
if not line.startswith('### `') or not line.endswith('`'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
elem = line[5:-2]
|
||||||
|
name, params = elem.split('[', 1)
|
||||||
|
if params:
|
||||||
|
params = _fix_param(params.split(';'))
|
||||||
|
else:
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if name in _hooks:
|
||||||
|
for p in reversed(tuple(map(copy.deepcopy, _hooks[name](params)))):
|
||||||
|
yield name, p
|
||||||
|
else:
|
||||||
|
yield name, params
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
"""
|
||||||
|
Returns a dict:
|
||||||
|
{
|
||||||
|
'element_name': [
|
||||||
|
['param1', 'param2'],
|
||||||
|
['alternate_params'],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
res = {}
|
||||||
|
for k, v in _raw_parse(data):
|
||||||
|
if k not in res:
|
||||||
|
res[k] = []
|
||||||
|
res[k].append(v)
|
||||||
|
|
||||||
|
for v in res.values():
|
||||||
|
v.sort(key=len, reverse=True)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
URL = 'https://github.com/minetest/minetest/raw/master/doc/lua_api.txt'
|
||||||
|
def fetch_and_parse(*, url=URL):
|
||||||
|
with urllib.request.urlopen(url) as f:
|
||||||
|
raw = f.read()
|
||||||
|
return parse(raw.decode('utf-8', 'replace'))
|
||||||
|
|
||||||
|
_comment = """
|
||||||
|
--
|
||||||
|
-- Formspec elements list. Do not update this by hand, it is auto-generated
|
||||||
|
-- by make_elements.py.
|
||||||
|
--
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dirname = os.path.dirname(__file__)
|
||||||
|
filename = os.path.join(dirname, 'elements.lua')
|
||||||
|
print('Writing to ' + filename + '...')
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(_comment.lstrip())
|
||||||
|
f.write(lua_dump.serialize(fetch_and_parse()))
|
||||||
|
# elems = fetch_and_parse()
|
||||||
|
# for elem in sorted(elems):
|
||||||
|
# for def_ in elems[elem]:
|
||||||
|
# f.write('formspec_ast.register_element({}, {})\n'.format(
|
||||||
|
# lua_dump.dump(elem), lua_dump.dump(def_)
|
||||||
|
# ))
|
||||||
|
print('Done.')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
2
mod.conf
Normal file
2
mod.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
name = formspec_ast
|
||||||
|
description = A mod library to help other mods interpret formspecs.
|
196
safety.lua
Normal file
196
safety.lua
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
--
|
||||||
|
-- formspec_ast: An abstract system tree for formspecs.
|
||||||
|
--
|
||||||
|
-- This verifies that formspecs from untrusted sources are safe(-ish) to
|
||||||
|
-- display, provided they are passed through formspec_ast.interpret.
|
||||||
|
--
|
||||||
|
-- The MIT License (MIT)
|
||||||
|
--
|
||||||
|
-- Copyright © 2019 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.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Similar to ast.walk(), however returns {} and then exits if walk() would
|
||||||
|
-- crash. Use this for untrusted formspecs, otherwise use walk() for speed.
|
||||||
|
local function safe_walk(tree)
|
||||||
|
local walk = formspec_ast.walk(tree)
|
||||||
|
local seen = {}
|
||||||
|
return function()
|
||||||
|
if not walk or not seen then return end
|
||||||
|
|
||||||
|
local good, msg = pcall(walk)
|
||||||
|
if good and (type(msg) == 'table' or msg == nil) and not seen[msg] then
|
||||||
|
if msg then
|
||||||
|
seen[msg] = true
|
||||||
|
end
|
||||||
|
return msg
|
||||||
|
else
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Similar to ast.flatten(), however removes unsafe elements.
|
||||||
|
local function safe_flatten(tree)
|
||||||
|
local res = {formspec_version = 1}
|
||||||
|
if tree.formspec_version == 2 then
|
||||||
|
res.formspec_version = 2
|
||||||
|
end
|
||||||
|
for elem in safe_walk(table.copy(tree)) do
|
||||||
|
if elem.type == 'container' then
|
||||||
|
if type(elem.x) == 'number' and type(elem.y) == 'number' then
|
||||||
|
formspec_ast.apply_offset(elem, elem.x, elem.y)
|
||||||
|
end
|
||||||
|
elseif elem.type then
|
||||||
|
table.insert(res, elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local ensure = {}
|
||||||
|
|
||||||
|
function ensure.string(obj)
|
||||||
|
if obj == nil then
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
return tostring(obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ensure.number(obj, max, min)
|
||||||
|
local res = tonumber(obj)
|
||||||
|
assert(res ~= nil and res == res)
|
||||||
|
assert(res <= (max or 100) and res >= (min or 0))
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function ensure.integer(obj)
|
||||||
|
return math.floor(ensure.number(obj))
|
||||||
|
end
|
||||||
|
|
||||||
|
local validate
|
||||||
|
local function validate_elem(obj)
|
||||||
|
local template = validate[obj.type]
|
||||||
|
assert(type(template) == 'table')
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
local func
|
||||||
|
if k == 'type' then
|
||||||
|
func = ensure.string
|
||||||
|
else
|
||||||
|
local type_ = template[k]
|
||||||
|
if type(type_) == 'string' then
|
||||||
|
if type_:sub(#type_) == '?' then
|
||||||
|
type_ = type_:sub(1, #type_ - 1)
|
||||||
|
end
|
||||||
|
func = ensure[type_]
|
||||||
|
elseif type(type_) == 'function' then
|
||||||
|
func = type_
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if func then
|
||||||
|
obj[k] = func(v)
|
||||||
|
else
|
||||||
|
obj[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(template) do
|
||||||
|
if type(v) ~= 'string' or v:sub(#v) ~= '?' then
|
||||||
|
assert(obj[k] ~= nil, k .. ' does not exist!')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validate = {
|
||||||
|
size = {w = 'number', h = 'number'},
|
||||||
|
label = {x = 'number', y = 'number', label = 'string'},
|
||||||
|
image = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
texture_name = 'string'},
|
||||||
|
button = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
name = 'string', label = 'string'},
|
||||||
|
field = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
name = 'string', label = 'string', default = 'string'},
|
||||||
|
pwdfield = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
name = 'string', label = 'string'},
|
||||||
|
field_close_on_enter = {name = 'string', close_on_enter = 'string'},
|
||||||
|
textarea = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
name = 'string', label = 'string', default = 'string'},
|
||||||
|
dropdown = {
|
||||||
|
x = 'number', y = 'number', w = 'number', name = 'string',
|
||||||
|
items = function(items)
|
||||||
|
assert(type(items) == 'list')
|
||||||
|
for k, v in pairs(items) do
|
||||||
|
assert(type(k) == 'number' and type(v) == 'string')
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
selected_idx = 'integer',
|
||||||
|
},
|
||||||
|
checkbox = {x = 'number', y = 'number', name = 'string', label = 'string',
|
||||||
|
selected = 'string'},
|
||||||
|
box = {x = 'number', y = 'number', w = 'number', h = 'number',
|
||||||
|
color = 'string'},
|
||||||
|
|
||||||
|
list = {
|
||||||
|
inventory_location = function(location)
|
||||||
|
assert(location == 'current_node' or location == 'current_player')
|
||||||
|
return location
|
||||||
|
end,
|
||||||
|
list_name = 'string', x = 'number', y = 'number', w = 'number',
|
||||||
|
h = 'number', starting_item_index = 'number?',
|
||||||
|
},
|
||||||
|
listring = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
validate.vertlabel = validate.label
|
||||||
|
validate.button_exit = validate.button
|
||||||
|
validate.image_button = validate.button
|
||||||
|
validate.image_button_exit = validate.button
|
||||||
|
validate.item_image_button = validate.button
|
||||||
|
|
||||||
|
-- Ensure that an AST tree is safe to display. The resulting tree will be
|
||||||
|
-- flattened for simplicity.
|
||||||
|
function formspec_ast.safe_parse(tree, custom_handlers)
|
||||||
|
if type(tree) == 'string' then
|
||||||
|
tree = formspec_ast.parse(tree, custom_handlers)
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(tree) ~= 'table' then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flatten the tree and remove objects that can't possibly be elements.
|
||||||
|
tree = safe_flatten(tree)
|
||||||
|
|
||||||
|
-- Iterate over the tree and add valid elements to a new table.
|
||||||
|
local res = {formspec_version = tree.formspec_version}
|
||||||
|
for i, elem in ipairs(tree) do
|
||||||
|
local good, msg = pcall(validate_elem, elem)
|
||||||
|
if good then
|
||||||
|
res[#res + 1] = elem
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function formspec_ast.safe_interpret(tree)
|
||||||
|
return formspec_ast.unparse(formspec_ast.safe_parse(tree))
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user