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