From d19dca03fe47cab97a620353d1e30098161f3d6c Mon Sep 17 00:00:00 2001 From: luk3yx Date: Sat, 1 Feb 2020 16:57:03 +1300 Subject: [PATCH] Refactor and add .luacheckrc. --- .luacheckrc | 14 ++ index.lua | 439 ++++----------------------------------------------- json.lua | 4 +- renderer.lua | 405 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 455 insertions(+), 407 deletions(-) create mode 100644 .luacheckrc create mode 100644 renderer.lua 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