Refactor and add .luacheckrc.
This commit is contained in:
parent
f5bbf9b839
commit
d19dca03fe
|
@ -0,0 +1,14 @@
|
||||||
|
unused_args = false
|
||||||
|
allow_defined_top = true
|
||||||
|
|
||||||
|
read_globals = {
|
||||||
|
formspec_ast = {
|
||||||
|
fields = {"_raw_parse", "_raw_unparse", "apply_offset", "find",
|
||||||
|
"flatten", "formspec_escape", "get_element_by_name",
|
||||||
|
"get_elements_by_name", "interpret", "parse", "register_element",
|
||||||
|
"safe_interpret", "safe_parse", "show_formspec", "unparse", "walk"}
|
||||||
|
},
|
||||||
|
fs51 = {
|
||||||
|
fields = {"backport", "backport_tree"}
|
||||||
|
},
|
||||||
|
}
|
439
index.lua
439
index.lua
|
@ -17,24 +17,10 @@
|
||||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Load formspec_ast
|
-- Load the renderer
|
||||||
FORMSPEC_AST_PATH = 'formspec_ast'
|
dofile('renderer.lua')
|
||||||
dofile(FORMSPEC_AST_PATH .. '/init.lua')
|
|
||||||
local formspec_escape = formspec_ast.formspec_escape
|
local formspec_escape = formspec_ast.formspec_escape
|
||||||
|
|
||||||
-- Load fs51 to allow formspec_version[1] exports
|
|
||||||
FS51_PATH = 'fs51'
|
|
||||||
dofile(FS51_PATH .. '/init.lua')
|
|
||||||
|
|
||||||
-- Load the JSON interoperability code
|
|
||||||
dofile('json.lua')
|
|
||||||
|
|
||||||
local js = require 'js'
|
|
||||||
local window = js.global
|
|
||||||
local document = window.document
|
|
||||||
|
|
||||||
renderer = {}
|
|
||||||
|
|
||||||
-- Show the properties list
|
-- Show the properties list
|
||||||
local properties_elem
|
local properties_elem
|
||||||
local function get_properties_list(list_name)
|
local function get_properties_list(list_name)
|
||||||
|
@ -90,7 +76,6 @@ local function show_properties(elem, node)
|
||||||
-- This table generation code is bad, the entire properties
|
-- This table generation code is bad, the entire properties
|
||||||
-- formspec is redrawn when a table element is deleted/created,
|
-- formspec is redrawn when a table element is deleted/created,
|
||||||
-- however the "reset" button works.
|
-- however the "reset" button works.
|
||||||
local k = k
|
|
||||||
formspec = formspec .. 'label[0.25,' .. y - 0.2 .. ';' ..
|
formspec = formspec .. 'label[0.25,' .. y - 0.2 .. ';' ..
|
||||||
formspec_escape(get_property_name(k)) .. ' (list)]'
|
formspec_escape(get_property_name(k)) .. ' (list)]'
|
||||||
y = y + 0.1
|
y = y + 0.1
|
||||||
|
@ -102,8 +87,6 @@ local function show_properties(elem, node)
|
||||||
'button[5.15,' .. y .. ';0.6,0.6;' ..
|
'button[5.15,' .. y .. ';0.6,0.6;' ..
|
||||||
formspec_escape('list-' .. i .. ':' .. k) .. ';X]'
|
formspec_escape('list-' .. i .. ':' .. k) .. ';X]'
|
||||||
y = y + 0.8
|
y = y + 0.8
|
||||||
|
|
||||||
local i = i
|
|
||||||
callbacks['list-' .. i .. ':' .. k] = function()
|
callbacks['list-' .. i .. ':' .. k] = function()
|
||||||
node[k] = get_properties_list(k)
|
node[k] = get_properties_list(k)
|
||||||
table.remove(node[k], i)
|
table.remove(node[k], i)
|
||||||
|
@ -149,8 +132,15 @@ local function show_properties(elem, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
formspec = 'formspec_version[2]size[6,' .. y + 1.25 .. ']' .. formspec ..
|
formspec = 'formspec_version[2]size[6,' .. y + 1.25 .. ']' .. formspec ..
|
||||||
'button[0.25,' .. y.. ';5.5,1;save;' ..
|
'button[0.25,' .. y.. ';5.5,1;save;'
|
||||||
(node._transient and 'Create element' or 'Save changes') .. ']'
|
if node.type == 'size' then
|
||||||
|
formspec = formspec .. 'Resize formspec'
|
||||||
|
elseif node._transient then
|
||||||
|
formspec = formspec .. 'Create element'
|
||||||
|
else
|
||||||
|
formspec = formspec .. 'Save changes'
|
||||||
|
end
|
||||||
|
formspec = formspec .. ']'
|
||||||
|
|
||||||
function callbacks.delete()
|
function callbacks.delete()
|
||||||
if js.global:confirm('Are you sure?') then
|
if js.global:confirm('Are you sure?') then
|
||||||
|
@ -169,24 +159,24 @@ local function show_properties(elem, node)
|
||||||
function callbacks.save()
|
function callbacks.save()
|
||||||
local elems = properties_elem.firstChild.firstChild.children
|
local elems = properties_elem.firstChild.firstChild.children
|
||||||
for i = 0, elems.length - 1 do
|
for i = 0, elems.length - 1 do
|
||||||
local elem = elems[i]
|
local e = elems[i]
|
||||||
local name = elem:getAttribute('data-formspec_ast-name')
|
local name = e:getAttribute('data-formspec_ast-name')
|
||||||
local prefix = type(name) == 'string' and name:sub(1, 5)
|
local prefix = type(name) == 'string' and name:sub(1, 5)
|
||||||
if prefix == 'prop_' then
|
if prefix == 'prop_' then
|
||||||
local k = name:sub(6)
|
local k = name:sub(6)
|
||||||
if type(node[k]) == 'string' then
|
if type(node[k]) == 'string' then
|
||||||
node[k] = elem.lastChild.value
|
node[k] = e.lastChild.value
|
||||||
elseif type(node[k]) == 'number' then
|
elseif type(node[k]) == 'number' then
|
||||||
-- Allow commas to be used as decimal points.
|
-- Allow commas to be used as decimal points.
|
||||||
local raw = elem.lastChild.value:gsub(',', '.')
|
local raw = e.lastChild.value:gsub(',', '.')
|
||||||
node[k] = tonumber(raw) or node[k]
|
node[k] = tonumber(raw) or node[k]
|
||||||
elseif type(node[k]) == 'boolean' then
|
elseif type(node[k]) == 'boolean' then
|
||||||
node[k] = elem:getAttribute('data-checked') == 'true'
|
node[k] = e:getAttribute('data-checked') == 'true'
|
||||||
end
|
end
|
||||||
elseif prefix == 'list[' then
|
elseif prefix == 'list[' then
|
||||||
local s, e = name:find(']', nil, true)
|
local s = name:find(']', nil, true)
|
||||||
local k = name:sub(e + 1)
|
local k = name:sub(s + 1)
|
||||||
node[k][tonumber(name:sub(6, s - 1))] = elem.lastChild.value
|
node[k][tonumber(name:sub(6, s - 1))] = e.lastChild.value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
node._transient = nil
|
node._transient = nil
|
||||||
|
@ -212,306 +202,7 @@ local function show_properties(elem, node)
|
||||||
local n = assert(renderer.render_formspec(formspec, callbacks, false))
|
local n = assert(renderer.render_formspec(formspec, callbacks, false))
|
||||||
properties_elem:appendChild(n)
|
properties_elem:appendChild(n)
|
||||||
end
|
end
|
||||||
|
renderer.default_callback = show_properties
|
||||||
-- Render formspecs to HTML
|
|
||||||
local elems = {}
|
|
||||||
|
|
||||||
local function update(src, dest)
|
|
||||||
for k, v in pairs(src) do
|
|
||||||
if type(v) == 'table' and dest[k] then
|
|
||||||
update(dest[k], v)
|
|
||||||
else
|
|
||||||
dest[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function make(elem_type, props, attrs)
|
|
||||||
local elem = document:createElement(elem_type)
|
|
||||||
if props then
|
|
||||||
update(props, elem)
|
|
||||||
end
|
|
||||||
if attrs then
|
|
||||||
for k, v in pairs(attrs) do
|
|
||||||
elem:setAttribute(k:gsub('_', '-'), v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return elem
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.label(node)
|
|
||||||
return make('span', {
|
|
||||||
textContent = node.label,
|
|
||||||
}, {
|
|
||||||
data_text = node.label,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.button(node)
|
|
||||||
return make('div', {
|
|
||||||
textContent = node.label,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
elems.button_exit = elems.button
|
|
||||||
|
|
||||||
function elems.image_button(node)
|
|
||||||
local res = make('div', nil, {
|
|
||||||
data_drawborder = tostring(node.drawborder ~= false),
|
|
||||||
})
|
|
||||||
res:appendChild(renderer.make_image(node.texture_name, true))
|
|
||||||
res:appendChild(make('span', {textContent = node.label}))
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
elems.image_button_exit = elems.image_button
|
|
||||||
|
|
||||||
local function make_field(input_type, node, base, default_callbacks)
|
|
||||||
local res = make('div')
|
|
||||||
res:appendChild(make('span', {textContent = node.label}))
|
|
||||||
local input = make('input', nil, {
|
|
||||||
type = input_type,
|
|
||||||
value = node.default or '',
|
|
||||||
})
|
|
||||||
if default_callbacks then
|
|
||||||
input:setAttribute('readonly', 'readonly')
|
|
||||||
end
|
|
||||||
res:appendChild(input)
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.field(...)
|
|
||||||
return make_field('text', ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.pwdfield(...)
|
|
||||||
return make_field('password', ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.textarea(node, base, default_callbacks)
|
|
||||||
local res = make('div')
|
|
||||||
res:appendChild(make('span', {textContent = node.label}))
|
|
||||||
local textarea = make('textarea', nil, {
|
|
||||||
type = 'text',
|
|
||||||
})
|
|
||||||
textarea.textContent = node.default or ''
|
|
||||||
if default_callbacks then
|
|
||||||
textarea:setAttribute('readonly', 'readonly')
|
|
||||||
end
|
|
||||||
res:appendChild(textarea)
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.size(node, base, default_callbacks, scale)
|
|
||||||
base.style.width = (node.w * scale) .. 'px'
|
|
||||||
base.style.height = (node.h * scale) .. 'px'
|
|
||||||
|
|
||||||
base:setAttribute('data-w', tostring(node.w))
|
|
||||||
base:setAttribute('data-h', tostring(node.h))
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.image(node)
|
|
||||||
return renderer.make_image(node.texture_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.checkbox(node, base, default_callbacks)
|
|
||||||
local checked = node.selected
|
|
||||||
local div = make('div', nil, {data_checked = tostring(checked)})
|
|
||||||
div:appendChild(make('div'))
|
|
||||||
div:appendChild(make('span', {textContent = node.label}))
|
|
||||||
if not default_callbacks then
|
|
||||||
div:addEventListener('click', function()
|
|
||||||
checked = not checked
|
|
||||||
div:setAttribute('data-checked', tostring(checked))
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
return div
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.list(node, base, default_callbacks)
|
|
||||||
local w, h = math.floor(node.w), math.floor(node.h)
|
|
||||||
local res = make('table')
|
|
||||||
for y = 1, h do
|
|
||||||
local tr = make('tr')
|
|
||||||
for x = 1, w do
|
|
||||||
tr:appendChild(make('td'))
|
|
||||||
end
|
|
||||||
res:appendChild(tr)
|
|
||||||
end
|
|
||||||
res.style.left = node.x .. 'em'
|
|
||||||
res.style.top = node.y .. 'em'
|
|
||||||
res.style.width = (node.w * 1.25) .. 'em'
|
|
||||||
res.style.height = (node.h * 1.25) .. 'em'
|
|
||||||
return res, true
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.box(node)
|
|
||||||
local res = make('div')
|
|
||||||
res.style.backgroundColor = node.color
|
|
||||||
if node.color:find('^ *rgb[^a]') or
|
|
||||||
node.color:find('^ *#..[^ ] *$') or
|
|
||||||
node.color:find('^ *#.....[^ ] *$') then
|
|
||||||
res.style.opacity = '0.55'
|
|
||||||
end
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
function elems.textlist(node)
|
|
||||||
local res = make('div')
|
|
||||||
for i, item in ipairs(node.listelem) do
|
|
||||||
local elem = make('div')
|
|
||||||
if item:sub(1, 1) ~= '#' then
|
|
||||||
elem.textContent = item
|
|
||||||
elseif item:sub(2, 2) == '#' then
|
|
||||||
elem.textContent = item:sub(2)
|
|
||||||
else
|
|
||||||
elem.style.color = item:sub(1, 7)
|
|
||||||
elem.textContent = item:sub(8)
|
|
||||||
end
|
|
||||||
if elem.textContent == '' then
|
|
||||||
elem.innerHTML = ' '
|
|
||||||
end
|
|
||||||
if i == node.selected_idx then
|
|
||||||
elem.style.background = '#467832';
|
|
||||||
end
|
|
||||||
res:appendChild(elem)
|
|
||||||
end
|
|
||||||
if node.transparent then
|
|
||||||
res.style.background = 'none'
|
|
||||||
res.style.borderColor = 'transparent'
|
|
||||||
end
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Make images - This uses HDX to simplify things
|
|
||||||
local image_baseurl = 'https://gitlab.com/VanessaE/hdx-128/raw/master/'
|
|
||||||
function renderer.make_image(name, allow_empty)
|
|
||||||
-- Remove extension
|
|
||||||
local real_name = name:match('^(.*)%.[^%.]+$') or ''
|
|
||||||
|
|
||||||
-- Make an <img> element
|
|
||||||
local img = document:createElement('img')
|
|
||||||
local mode = 'png'
|
|
||||||
img:setAttribute('ondragstart', 'return false')
|
|
||||||
if name == '' and allow_empty then
|
|
||||||
img.style.opacity = '0'
|
|
||||||
return img
|
|
||||||
end
|
|
||||||
img:addEventListener('error', function()
|
|
||||||
if mode == 'png' then
|
|
||||||
mode = 'jpg'
|
|
||||||
elseif mode == nil then
|
|
||||||
return
|
|
||||||
else
|
|
||||||
mode = nil
|
|
||||||
img.src = image_baseurl .. 'unknown_node.png'
|
|
||||||
return
|
|
||||||
end
|
|
||||||
img.src = image_baseurl .. real_name .. '.' .. mode
|
|
||||||
end)
|
|
||||||
img.src = image_baseurl .. real_name .. '.' .. mode
|
|
||||||
return img
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.render_ast(tree, callbacks, store_json, scale)
|
|
||||||
scale = 50 * (scale or 1)
|
|
||||||
local base = document:createElement('div')
|
|
||||||
base.className = 'formspec_ast-base'
|
|
||||||
base.style.fontSize = scale .. 'px'
|
|
||||||
local container = document:createElement('div')
|
|
||||||
base:appendChild(container)
|
|
||||||
|
|
||||||
for _, node in ipairs(formspec_ast.flatten(tree)) do
|
|
||||||
if not elems[node.type] then
|
|
||||||
return nil, 'Formspec element type ' .. node.type ..
|
|
||||||
' not implemented.'
|
|
||||||
end
|
|
||||||
local e, ignore_pos = elems[node.type](node, base, callbacks == nil,
|
|
||||||
scale)
|
|
||||||
if e then
|
|
||||||
if node.x and node.y and not ignore_pos then
|
|
||||||
e.style.left = (node.x * scale) .. 'px'
|
|
||||||
e.style.top = (node.y * scale) .. 'px'
|
|
||||||
if node.w and node.h then
|
|
||||||
e.style.width = (node.w * scale) .. 'px'
|
|
||||||
e.style.height = (node.h * scale) .. 'px'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
e.className = 'formspec_ast-element formspec_ast-' .. node.type
|
|
||||||
if store_json or store_json == nil then
|
|
||||||
e:setAttribute('data-formspec_ast', json.dumps(node))
|
|
||||||
end
|
|
||||||
if node.name then
|
|
||||||
e:setAttribute('data-formspec_ast-name', node.name)
|
|
||||||
end
|
|
||||||
local func
|
|
||||||
if type(callbacks) == 'table' then
|
|
||||||
func = callbacks[node.name or '']
|
|
||||||
elseif callbacks == nil then
|
|
||||||
func = show_properties
|
|
||||||
end
|
|
||||||
if func then
|
|
||||||
e:addEventListener('click', func)
|
|
||||||
e.className = e.className .. ' formspec_ast-clickable'
|
|
||||||
end
|
|
||||||
container:appendChild(e)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
container.style.width = base.style.width
|
|
||||||
container.style.height = base.style.height
|
|
||||||
return base
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.render_formspec(formspec, ...)
|
|
||||||
local tree, err = formspec_ast.parse(formspec)
|
|
||||||
if err then
|
|
||||||
return nil, err
|
|
||||||
end
|
|
||||||
return renderer.render_ast(tree, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.elem_to_ast(elem)
|
|
||||||
assert(elem.children.length == 1)
|
|
||||||
local elems = elem.firstChild.children
|
|
||||||
|
|
||||||
local w = tonumber(elem:getAttribute('data-w'))
|
|
||||||
local h = tonumber(elem:getAttribute('data-h'))
|
|
||||||
local res = {
|
|
||||||
formspec_version = 2,
|
|
||||||
{
|
|
||||||
type = 'size',
|
|
||||||
w = w or 0,
|
|
||||||
h = h or 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i = 0, elems.length - 1 do
|
|
||||||
local data = elems[i]:getAttribute('data-formspec_ast')
|
|
||||||
local node = assert(json.loads(data), 'Error loading data!')
|
|
||||||
|
|
||||||
if not node._transient then
|
|
||||||
if node.name == 'size' then
|
|
||||||
-- A hack to replace the existing size[] with any new one
|
|
||||||
res[2] = node
|
|
||||||
else
|
|
||||||
res[#res + 1] = node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
local element_dialog_base
|
|
||||||
local function replace_formspec(elem, tree)
|
|
||||||
local new_elem, err = renderer.render_ast(tree)
|
|
||||||
if not new_elem then return nil, err end
|
|
||||||
elem:replaceWith(new_elem)
|
|
||||||
if element_dialog_base == elem then
|
|
||||||
element_dialog_base = new_elem
|
|
||||||
end
|
|
||||||
return new_elem, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.redraw_formspec(elem)
|
|
||||||
replace_formspec(elem, renderer.elem_to_ast(elem))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Templates for new elements
|
-- Templates for new elements
|
||||||
do
|
do
|
||||||
|
@ -540,7 +231,7 @@ end
|
||||||
function renderer.add_element(base, node_type)
|
function renderer.add_element(base, node_type)
|
||||||
local elem = base.firstChild.lastChild
|
local elem = base.firstChild.lastChild
|
||||||
if elem == js.null or elem:getAttribute('data-transient') ~= 'true' then
|
if elem == js.null or elem:getAttribute('data-transient') ~= 'true' then
|
||||||
elem = make('div')
|
elem = renderer.make('div')
|
||||||
elem.style.display = 'none'
|
elem.style.display = 'none'
|
||||||
base.firstChild:appendChild(elem)
|
base.firstChild:appendChild(elem)
|
||||||
end
|
end
|
||||||
|
@ -560,78 +251,16 @@ function renderer.add_element(base, node_type)
|
||||||
show_properties(elem)
|
show_properties(elem)
|
||||||
end
|
end
|
||||||
|
|
||||||
function renderer.unrender_formspec(elem)
|
local element_dialog_base
|
||||||
local res = renderer.elem_to_ast(elem)
|
do
|
||||||
return formspec_ast.unparse(res)
|
local replace_formspec = renderer.replace_formspec
|
||||||
end
|
function renderer.replace_formspec(elem, tree)
|
||||||
|
local new_elem, err = replace_formspec(elem, tree)
|
||||||
local load = rawget(_G, 'loadstring') or load
|
if new_elem and element_dialog_base == elem then
|
||||||
local function deserialize(code)
|
element_dialog_base = new_elem
|
||||||
if code:byte(1) == 0x1b then return nil, 'Cannot load bytecode' end
|
|
||||||
code = 'return ' .. code
|
|
||||||
local f
|
|
||||||
if rawget(_G, 'loadstring') and rawget(_G, 'setfenv') then
|
|
||||||
f = loadstring(code)
|
|
||||||
setfenv(f, {})
|
|
||||||
else
|
|
||||||
f = load(code, nil, nil, {})
|
|
||||||
end
|
|
||||||
local ok, res = pcall(f)
|
|
||||||
if ok then
|
|
||||||
return res, nil
|
|
||||||
else
|
|
||||||
return nil, res
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.import(fs, opts)
|
|
||||||
if opts.format then
|
|
||||||
fs = fs:gsub('" %.%. minetest.formspec_escape%(tostring%(' ..
|
|
||||||
'%-%-%[%[${%]%]([^}]*)%-%-%[%[}%]%]%)%) %.%. "', function(s)
|
|
||||||
return '${' .. ('%q'):format(s):sub(2, -2) .. '}'
|
|
||||||
end)
|
|
||||||
local err
|
|
||||||
local fs2 = fs
|
|
||||||
fs, err = deserialize(fs)
|
|
||||||
if type(fs) ~= 'string' then
|
|
||||||
return nil, err or 'That was valid Lua but not a valid formspec!'
|
|
||||||
end
|
end
|
||||||
elseif fs:sub(1, 1) == '"' then
|
return new_elem, err
|
||||||
return nil, 'Did you mean to enable ${...} conversion?'
|
|
||||||
end
|
end
|
||||||
local tree, err = formspec_ast.parse(fs)
|
|
||||||
if tree and tree.formspec_version < 2 then
|
|
||||||
return nil, 'Only formspec versions >= 2 can be loaded!'
|
|
||||||
end
|
|
||||||
return tree, err
|
|
||||||
end
|
|
||||||
|
|
||||||
function renderer.export(tree, opts)
|
|
||||||
if opts.use_v1 then
|
|
||||||
tree = fs51.backport(tree)
|
|
||||||
end
|
|
||||||
local fs, err = formspec_ast.unparse(tree)
|
|
||||||
if not fs then return nil, err end
|
|
||||||
if opts.format then
|
|
||||||
fs = ('%q'):format(fs)
|
|
||||||
local ok, msg = true, ''
|
|
||||||
fs = fs:gsub('${([^}]*)}', function(code)
|
|
||||||
code = assert(deserialize('"' .. code .. '"'))
|
|
||||||
if code:byte(1) == 0x1b then
|
|
||||||
ok, msg = false, 'Bytecode not permitted in format strings'
|
|
||||||
elseif ok then
|
|
||||||
ok, msg = load('return ' .. code)
|
|
||||||
end
|
|
||||||
-- This adds markers before and after the code so it can be
|
|
||||||
-- extracted easily in renderer.import().
|
|
||||||
return '" .. minetest.formspec_escape(tostring(--[[${]]' .. code ..
|
|
||||||
'--[[}]])) .. "'
|
|
||||||
end)
|
|
||||||
if not ok then
|
|
||||||
return nil, msg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return fs, nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function render_into(base, formspec, callbacks)
|
local function render_into(base, formspec, callbacks)
|
||||||
|
@ -644,7 +273,7 @@ local element_dialog
|
||||||
local load_save_opts = {}
|
local load_save_opts = {}
|
||||||
local function show_load_save_dialog()
|
local function show_load_save_dialog()
|
||||||
local callbacks = {}
|
local callbacks = {}
|
||||||
local fs = [[
|
local formspec = [[
|
||||||
formspec_version[2]size[6,9.5]button[0,0;1,0.6;back;←]
|
formspec_version[2]size[6,9.5]button[0,0;1,0.6;back;←]
|
||||||
label[1.25,0.3;Load / save formspec]
|
label[1.25,0.3;Load / save formspec]
|
||||||
checkbox[0.25,1.3;use_v1;Use formspec version 1;]] ..
|
checkbox[0.25,1.3;use_v1;Use formspec version 1;]] ..
|
||||||
|
@ -671,7 +300,6 @@ local function show_load_save_dialog()
|
||||||
load_save_opts[name] = checked == 'true'
|
load_save_opts[name] = checked == 'true'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return opts
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function callbacks.back()
|
function callbacks.back()
|
||||||
|
@ -687,7 +315,8 @@ local function show_load_save_dialog()
|
||||||
window:alert('Error loading formspec:\n' .. err)
|
window:alert('Error loading formspec:\n' .. err)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local elem, err = replace_formspec(element_dialog_base, tree)
|
local elem
|
||||||
|
elem, err = renderer.replace_formspec(element_dialog_base, tree)
|
||||||
if not elem then
|
if not elem then
|
||||||
window:alert('Error loading formspec:\n' .. err)
|
window:alert('Error loading formspec:\n' .. err)
|
||||||
return
|
return
|
||||||
|
@ -727,7 +356,7 @@ local function show_load_save_dialog()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
render_into(element_dialog, fs, callbacks)
|
render_into(element_dialog, formspec, callbacks)
|
||||||
end
|
end
|
||||||
|
|
||||||
function renderer.show_element_dialog(base)
|
function renderer.show_element_dialog(base)
|
||||||
|
@ -767,7 +396,7 @@ function window:render_formspec(fs, callbacks)
|
||||||
local tree = assert(formspec_ast.parse(fs))
|
local tree = assert(formspec_ast.parse(fs))
|
||||||
local elem = assert(renderer.render_ast(tree, callbacks))
|
local elem = assert(renderer.render_ast(tree, callbacks))
|
||||||
local e = document:getElementById('formspec_output')
|
local e = document:getElementById('formspec_output')
|
||||||
if not e or e == null then
|
if not e or e == js.null then
|
||||||
window:addEventListener('load', function()
|
window:addEventListener('load', function()
|
||||||
window:render_formspec(fs, callbacks)
|
window:render_formspec(fs, callbacks)
|
||||||
end)
|
end)
|
||||||
|
|
4
json.lua
4
json.lua
|
@ -57,8 +57,8 @@ end
|
||||||
|
|
||||||
-- Alias for JSON:parse so pcall can call it.
|
-- Alias for JSON:parse so pcall can call it.
|
||||||
local function raw_parse(json, nullvalue)
|
local function raw_parse(json, nullvalue)
|
||||||
local json = JSON:parse(json)
|
local obj = JSON:parse(json)
|
||||||
return object_to_table(json, nullvalue)
|
return object_to_table(obj, nullvalue)
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.loads(json, nullvalue)
|
function json.loads(json, nullvalue)
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
--
|
||||||
|
-- Web-based formspec editor
|
||||||
|
--
|
||||||
|
-- Copyright © 2020 by luk3yx.
|
||||||
|
--
|
||||||
|
-- This program is free software: you can redistribute it and/or modify
|
||||||
|
-- it under the terms of the GNU Affero General Public License as
|
||||||
|
-- published by the Free Software Foundation, either version 3 of the
|
||||||
|
-- License, or (at your option) any later version.
|
||||||
|
--
|
||||||
|
-- This program is distributed in the hope that it will be useful,
|
||||||
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
-- GNU Affero General Public License for more details.
|
||||||
|
--
|
||||||
|
-- You should have received a copy of the GNU Affero General Public License
|
||||||
|
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Load formspec_ast
|
||||||
|
FORMSPEC_AST_PATH = 'formspec_ast'
|
||||||
|
dofile(FORMSPEC_AST_PATH .. '/init.lua')
|
||||||
|
|
||||||
|
-- Load fs51 to allow formspec_version[1] exports
|
||||||
|
FS51_PATH = 'fs51'
|
||||||
|
dofile(FS51_PATH .. '/init.lua')
|
||||||
|
|
||||||
|
-- Load the JSON interoperability code
|
||||||
|
dofile('json.lua')
|
||||||
|
|
||||||
|
js = require 'js'
|
||||||
|
window = js.global
|
||||||
|
document = window.document
|
||||||
|
|
||||||
|
renderer = {}
|
||||||
|
|
||||||
|
-- Render formspecs to HTML
|
||||||
|
local elems = {}
|
||||||
|
|
||||||
|
local function update(src, dest)
|
||||||
|
for k, v in pairs(src) do
|
||||||
|
if type(v) == 'table' and dest[k] then
|
||||||
|
update(dest[k], v)
|
||||||
|
else
|
||||||
|
dest[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make(elem_type, props, attrs)
|
||||||
|
local elem = document:createElement(elem_type)
|
||||||
|
if props then
|
||||||
|
update(props, elem)
|
||||||
|
end
|
||||||
|
if attrs then
|
||||||
|
for k, v in pairs(attrs) do
|
||||||
|
elem:setAttribute(k:gsub('_', '-'), v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return elem
|
||||||
|
end
|
||||||
|
renderer.make = make
|
||||||
|
|
||||||
|
function elems.label(node)
|
||||||
|
return make('span', {
|
||||||
|
textContent = node.label,
|
||||||
|
}, {
|
||||||
|
data_text = node.label,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.button(node)
|
||||||
|
return make('div', {
|
||||||
|
textContent = node.label,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
elems.button_exit = elems.button
|
||||||
|
|
||||||
|
function elems.image_button(node)
|
||||||
|
local res = make('div', nil, {
|
||||||
|
data_drawborder = tostring(node.drawborder ~= false),
|
||||||
|
})
|
||||||
|
res:appendChild(renderer.make_image(node.texture_name, true))
|
||||||
|
res:appendChild(make('span', {textContent = node.label}))
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
elems.image_button_exit = elems.image_button
|
||||||
|
|
||||||
|
local function make_field(input_type, node, base, default_callbacks)
|
||||||
|
local res = make('div')
|
||||||
|
res:appendChild(make('span', {textContent = node.label}))
|
||||||
|
local input = make('input', nil, {
|
||||||
|
type = input_type,
|
||||||
|
value = node.default or '',
|
||||||
|
})
|
||||||
|
if default_callbacks then
|
||||||
|
input:setAttribute('readonly', 'readonly')
|
||||||
|
end
|
||||||
|
res:appendChild(input)
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.field(...)
|
||||||
|
return make_field('text', ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.pwdfield(...)
|
||||||
|
return make_field('password', ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.textarea(node, base, default_callbacks)
|
||||||
|
local res = make('div')
|
||||||
|
res:appendChild(make('span', {textContent = node.label}))
|
||||||
|
local textarea = make('textarea', nil, {
|
||||||
|
type = 'text',
|
||||||
|
})
|
||||||
|
textarea.textContent = node.default or ''
|
||||||
|
if default_callbacks then
|
||||||
|
textarea:setAttribute('readonly', 'readonly')
|
||||||
|
end
|
||||||
|
res:appendChild(textarea)
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.size(node, base, default_callbacks, scale)
|
||||||
|
base.style.width = (node.w * scale) .. 'px'
|
||||||
|
base.style.height = (node.h * scale) .. 'px'
|
||||||
|
|
||||||
|
base:setAttribute('data-w', tostring(node.w))
|
||||||
|
base:setAttribute('data-h', tostring(node.h))
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.image(node)
|
||||||
|
return renderer.make_image(node.texture_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.checkbox(node, base, default_callbacks)
|
||||||
|
local checked = node.selected
|
||||||
|
local div = make('div', nil, {data_checked = tostring(checked)})
|
||||||
|
div:appendChild(make('div'))
|
||||||
|
div:appendChild(make('span', {textContent = node.label}))
|
||||||
|
if not default_callbacks then
|
||||||
|
div:addEventListener('click', function()
|
||||||
|
checked = not checked
|
||||||
|
div:setAttribute('data-checked', tostring(checked))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return div
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.list(node, base, default_callbacks)
|
||||||
|
local w, h = math.floor(node.w), math.floor(node.h)
|
||||||
|
local res = make('table')
|
||||||
|
for y = 1, h do
|
||||||
|
local tr = make('tr')
|
||||||
|
for x = 1, w do
|
||||||
|
tr:appendChild(make('td'))
|
||||||
|
end
|
||||||
|
res:appendChild(tr)
|
||||||
|
end
|
||||||
|
res.style.left = node.x .. 'em'
|
||||||
|
res.style.top = node.y .. 'em'
|
||||||
|
res.style.width = (node.w * 1.25) .. 'em'
|
||||||
|
res.style.height = (node.h * 1.25) .. 'em'
|
||||||
|
return res, true
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.box(node)
|
||||||
|
local res = make('div')
|
||||||
|
res.style.backgroundColor = node.color
|
||||||
|
if node.color:find('^ *rgb[^a]') or
|
||||||
|
node.color:find('^ *#..[^ ] *$') or
|
||||||
|
node.color:find('^ *#.....[^ ] *$') then
|
||||||
|
res.style.opacity = '0.55'
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function elems.textlist(node)
|
||||||
|
local res = make('div')
|
||||||
|
for i, item in ipairs(node.listelem) do
|
||||||
|
local elem = make('div')
|
||||||
|
if item:sub(1, 1) ~= '#' then
|
||||||
|
elem.textContent = item
|
||||||
|
elseif item:sub(2, 2) == '#' then
|
||||||
|
elem.textContent = item:sub(2)
|
||||||
|
else
|
||||||
|
elem.style.color = item:sub(1, 7)
|
||||||
|
elem.textContent = item:sub(8)
|
||||||
|
end
|
||||||
|
if elem.textContent == '' then
|
||||||
|
elem.innerHTML = ' '
|
||||||
|
end
|
||||||
|
if i == node.selected_idx then
|
||||||
|
elem.style.background = '#467832';
|
||||||
|
end
|
||||||
|
res:appendChild(elem)
|
||||||
|
end
|
||||||
|
if node.transparent then
|
||||||
|
res.style.background = 'none'
|
||||||
|
res.style.borderColor = 'transparent'
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make images - This uses HDX to simplify things
|
||||||
|
local image_baseurl = 'https://gitlab.com/VanessaE/hdx-128/raw/master/'
|
||||||
|
function renderer.make_image(name, allow_empty)
|
||||||
|
-- Remove extension
|
||||||
|
local real_name = name:match('^(.*)%.[^%.]+$') or ''
|
||||||
|
|
||||||
|
-- Make an <img> element
|
||||||
|
local img = document:createElement('img')
|
||||||
|
local mode = 'png'
|
||||||
|
img:setAttribute('ondragstart', 'return false')
|
||||||
|
if name == '' and allow_empty then
|
||||||
|
img.style.opacity = '0'
|
||||||
|
return img
|
||||||
|
end
|
||||||
|
img:addEventListener('error', function()
|
||||||
|
if mode == 'png' then
|
||||||
|
mode = 'jpg'
|
||||||
|
elseif mode == nil then
|
||||||
|
return
|
||||||
|
else
|
||||||
|
mode = nil
|
||||||
|
img.src = image_baseurl .. 'unknown_node.png'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
img.src = image_baseurl .. real_name .. '.' .. mode
|
||||||
|
end)
|
||||||
|
img.src = image_baseurl .. real_name .. '.' .. mode
|
||||||
|
return img
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.render_ast(tree, callbacks, store_json, scale)
|
||||||
|
scale = 50 * (scale or 1)
|
||||||
|
local base = document:createElement('div')
|
||||||
|
base.className = 'formspec_ast-base'
|
||||||
|
base.style.fontSize = scale .. 'px'
|
||||||
|
local container = document:createElement('div')
|
||||||
|
base:appendChild(container)
|
||||||
|
|
||||||
|
for _, node in ipairs(formspec_ast.flatten(tree)) do
|
||||||
|
if not elems[node.type] then
|
||||||
|
return nil, 'Formspec element type ' .. node.type ..
|
||||||
|
' not implemented.'
|
||||||
|
end
|
||||||
|
local e, ignore_pos = elems[node.type](node, base, callbacks == nil,
|
||||||
|
scale)
|
||||||
|
if e then
|
||||||
|
if node.x and node.y and not ignore_pos then
|
||||||
|
e.style.left = (node.x * scale) .. 'px'
|
||||||
|
e.style.top = (node.y * scale) .. 'px'
|
||||||
|
if node.w and node.h then
|
||||||
|
e.style.width = (node.w * scale) .. 'px'
|
||||||
|
e.style.height = (node.h * scale) .. 'px'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
e.className = 'formspec_ast-element formspec_ast-' .. node.type
|
||||||
|
if store_json or store_json == nil then
|
||||||
|
e:setAttribute('data-formspec_ast', json.dumps(node))
|
||||||
|
end
|
||||||
|
if node.name then
|
||||||
|
e:setAttribute('data-formspec_ast-name', node.name)
|
||||||
|
end
|
||||||
|
local func
|
||||||
|
if type(callbacks) == 'table' then
|
||||||
|
func = callbacks[node.name or '']
|
||||||
|
elseif callbacks == nil then
|
||||||
|
func = renderer.default_callback
|
||||||
|
end
|
||||||
|
if func then
|
||||||
|
e:addEventListener('click', func)
|
||||||
|
e.className = e.className .. ' formspec_ast-clickable'
|
||||||
|
end
|
||||||
|
container:appendChild(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
container.style.width = base.style.width
|
||||||
|
container.style.height = base.style.height
|
||||||
|
return base
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.render_formspec(formspec, ...)
|
||||||
|
local tree, err = formspec_ast.parse(formspec)
|
||||||
|
if err then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
return renderer.render_ast(tree, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.elem_to_ast(elem)
|
||||||
|
assert(elem.children.length == 1)
|
||||||
|
local html_elems = elem.firstChild.children
|
||||||
|
|
||||||
|
local w = tonumber(elem:getAttribute('data-w'))
|
||||||
|
local h = tonumber(elem:getAttribute('data-h'))
|
||||||
|
local res = {
|
||||||
|
formspec_version = 2,
|
||||||
|
{
|
||||||
|
type = 'size',
|
||||||
|
w = w or 0,
|
||||||
|
h = h or 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i = 0, html_elems.length - 1 do
|
||||||
|
local data = html_elems[i]:getAttribute('data-formspec_ast')
|
||||||
|
local node = assert(json.loads(data), 'Error loading data!')
|
||||||
|
|
||||||
|
if not node._transient then
|
||||||
|
if node.name == 'size' then
|
||||||
|
-- A hack to replace the existing size[] with any new one
|
||||||
|
res[2] = node
|
||||||
|
else
|
||||||
|
res[#res + 1] = node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.replace_formspec(elem, tree)
|
||||||
|
local new_elem, err = renderer.render_ast(tree)
|
||||||
|
if not new_elem then return nil, err end
|
||||||
|
elem:replaceWith(new_elem)
|
||||||
|
return new_elem, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.redraw_formspec(elem)
|
||||||
|
renderer.replace_formspec(elem, renderer.elem_to_ast(elem))
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.unrender_formspec(elem)
|
||||||
|
local res = renderer.elem_to_ast(elem)
|
||||||
|
return formspec_ast.unparse(res)
|
||||||
|
end
|
||||||
|
|
||||||
|
local load = rawget(_G, 'loadstring') or load
|
||||||
|
local function deserialize(code)
|
||||||
|
if code:byte(1) == 0x1b then return nil, 'Cannot load bytecode' end
|
||||||
|
code = 'return ' .. code
|
||||||
|
local f
|
||||||
|
if rawget(_G, 'loadstring') and rawget(_G, 'setfenv') then
|
||||||
|
f = loadstring(code)
|
||||||
|
setfenv(f, {})
|
||||||
|
else
|
||||||
|
f = load(code, nil, nil, {})
|
||||||
|
end
|
||||||
|
local ok, res = pcall(f)
|
||||||
|
if ok then
|
||||||
|
return res, nil
|
||||||
|
else
|
||||||
|
return nil, res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.import(fs, opts)
|
||||||
|
if opts.format then
|
||||||
|
fs = fs:gsub('" %.%. minetest.formspec_escape%(tostring%(' ..
|
||||||
|
'%-%-%[%[${%]%]([^}]*)%-%-%[%[}%]%]%)%) %.%. "', function(s)
|
||||||
|
return '${' .. ('%q'):format(s):sub(2, -2) .. '}'
|
||||||
|
end)
|
||||||
|
local err
|
||||||
|
fs, err = deserialize(fs)
|
||||||
|
if type(fs) ~= 'string' then
|
||||||
|
return nil, err or 'That was valid Lua but not a valid formspec!'
|
||||||
|
end
|
||||||
|
elseif fs:sub(1, 1) == '"' then
|
||||||
|
return nil, 'Did you mean to enable ${...} conversion?'
|
||||||
|
end
|
||||||
|
local tree, err = formspec_ast.parse(fs)
|
||||||
|
if tree and tree.formspec_version < 2 then
|
||||||
|
return nil, 'Only formspec versions >= 2 can be loaded!'
|
||||||
|
end
|
||||||
|
return tree, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderer.export(tree, opts)
|
||||||
|
if opts.use_v1 then
|
||||||
|
tree = fs51.backport(tree)
|
||||||
|
end
|
||||||
|
local fs, err = formspec_ast.unparse(tree)
|
||||||
|
if not fs then return nil, err end
|
||||||
|
if opts.format then
|
||||||
|
fs = ('%q'):format(fs)
|
||||||
|
local ok, msg = true, ''
|
||||||
|
fs = fs:gsub('${([^}]*)}', function(code)
|
||||||
|
code = assert(deserialize('"' .. code .. '"'))
|
||||||
|
if code:byte(1) == 0x1b then
|
||||||
|
ok, msg = false, 'Bytecode not permitted in format strings'
|
||||||
|
elseif ok then
|
||||||
|
ok, msg = load('return ' .. code)
|
||||||
|
end
|
||||||
|
-- This adds markers before and after the code so it can be
|
||||||
|
-- extracted easily in renderer.import().
|
||||||
|
return '" .. minetest.formspec_escape(tostring(--[[${]]' .. code ..
|
||||||
|
'--[[}]])) .. "'
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
return nil, msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return fs, nil
|
||||||
|
end
|
Loading…
Reference in New Issue