diff --git a/.luacheckrc b/.luacheckrc
new file mode 100644
index 0000000..86f87f3
--- /dev/null
+++ b/.luacheckrc
@@ -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"}
+ },
+}
diff --git a/index.lua b/index.lua
index 376e4aa..2a29e6c 100644
--- a/index.lua
+++ b/index.lua
@@ -17,24 +17,10 @@
-- along with this program. If not, see .
--
--- Load formspec_ast
-FORMSPEC_AST_PATH = 'formspec_ast'
-dofile(FORMSPEC_AST_PATH .. '/init.lua')
+-- Load the renderer
+dofile('renderer.lua')
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
local properties_elem
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
-- formspec is redrawn when a table element is deleted/created,
-- however the "reset" button works.
- local k = k
formspec = formspec .. 'label[0.25,' .. y - 0.2 .. ';' ..
formspec_escape(get_property_name(k)) .. ' (list)]'
y = y + 0.1
@@ -102,8 +87,6 @@ local function show_properties(elem, node)
'button[5.15,' .. y .. ';0.6,0.6;' ..
formspec_escape('list-' .. i .. ':' .. k) .. ';X]'
y = y + 0.8
-
- local i = i
callbacks['list-' .. i .. ':' .. k] = function()
node[k] = get_properties_list(k)
table.remove(node[k], i)
@@ -149,8 +132,15 @@ local function show_properties(elem, node)
end
formspec = 'formspec_version[2]size[6,' .. y + 1.25 .. ']' .. formspec ..
- 'button[0.25,' .. y.. ';5.5,1;save;' ..
- (node._transient and 'Create element' or 'Save changes') .. ']'
+ 'button[0.25,' .. y.. ';5.5,1;save;'
+ 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()
if js.global:confirm('Are you sure?') then
@@ -169,24 +159,24 @@ local function show_properties(elem, node)
function callbacks.save()
local elems = properties_elem.firstChild.firstChild.children
for i = 0, elems.length - 1 do
- local elem = elems[i]
- local name = elem:getAttribute('data-formspec_ast-name')
+ local e = elems[i]
+ local name = e:getAttribute('data-formspec_ast-name')
local prefix = type(name) == 'string' and name:sub(1, 5)
if prefix == 'prop_' then
local k = name:sub(6)
if type(node[k]) == 'string' then
- node[k] = elem.lastChild.value
+ node[k] = e.lastChild.value
elseif type(node[k]) == 'number' then
-- 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]
elseif type(node[k]) == 'boolean' then
- node[k] = elem:getAttribute('data-checked') == 'true'
+ node[k] = e:getAttribute('data-checked') == 'true'
end
elseif prefix == 'list[' then
- local s, e = name:find(']', nil, true)
- local k = name:sub(e + 1)
- node[k][tonumber(name:sub(6, s - 1))] = elem.lastChild.value
+ local s = name:find(']', nil, true)
+ local k = name:sub(s + 1)
+ node[k][tonumber(name:sub(6, s - 1))] = e.lastChild.value
end
end
node._transient = nil
@@ -212,306 +202,7 @@ local function show_properties(elem, node)
local n = assert(renderer.render_formspec(formspec, callbacks, false))
properties_elem:appendChild(n)
end
-
--- 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 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
+renderer.default_callback = show_properties
-- Templates for new elements
do
@@ -540,7 +231,7 @@ end
function renderer.add_element(base, node_type)
local elem = base.firstChild.lastChild
if elem == js.null or elem:getAttribute('data-transient') ~= 'true' then
- elem = make('div')
+ elem = renderer.make('div')
elem.style.display = 'none'
base.firstChild:appendChild(elem)
end
@@ -560,78 +251,16 @@ function renderer.add_element(base, node_type)
show_properties(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
- 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!'
+local element_dialog_base
+do
+ local replace_formspec = renderer.replace_formspec
+ function renderer.replace_formspec(elem, tree)
+ local new_elem, err = replace_formspec(elem, tree)
+ if new_elem and element_dialog_base == elem then
+ element_dialog_base = new_elem
end
- elseif fs:sub(1, 1) == '"' then
- return nil, 'Did you mean to enable ${...} conversion?'
+ return new_elem, err
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
local function render_into(base, formspec, callbacks)
@@ -644,7 +273,7 @@ local element_dialog
local load_save_opts = {}
local function show_load_save_dialog()
local callbacks = {}
- local fs = [[
+ local formspec = [[
formspec_version[2]size[6,9.5]button[0,0;1,0.6;back;←]
label[1.25,0.3;Load / save formspec]
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'
end
end
- return opts
end
function callbacks.back()
@@ -687,7 +315,8 @@ local function show_load_save_dialog()
window:alert('Error loading formspec:\n' .. err)
return
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
window:alert('Error loading formspec:\n' .. err)
return
@@ -727,7 +356,7 @@ local function show_load_save_dialog()
})
end
- render_into(element_dialog, fs, callbacks)
+ render_into(element_dialog, formspec, callbacks)
end
function renderer.show_element_dialog(base)
@@ -767,7 +396,7 @@ function window:render_formspec(fs, callbacks)
local tree = assert(formspec_ast.parse(fs))
local elem = assert(renderer.render_ast(tree, callbacks))
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:render_formspec(fs, callbacks)
end)
diff --git a/json.lua b/json.lua
index edcc0f7..726a9f4 100644
--- a/json.lua
+++ b/json.lua
@@ -57,8 +57,8 @@ end
-- Alias for JSON:parse so pcall can call it.
local function raw_parse(json, nullvalue)
- local json = JSON:parse(json)
- return object_to_table(json, nullvalue)
+ local obj = JSON:parse(json)
+ return object_to_table(obj, nullvalue)
end
function json.loads(json, nullvalue)
diff --git a/renderer.lua b/renderer.lua
new file mode 100644
index 0000000..6a184ec
--- /dev/null
+++ b/renderer.lua
@@ -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 .
+--
+
+-- 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 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