diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 817e409..05ed1f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,17 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v3 - name: lint uses: Roang-zero1/factorio-mod-luacheck@master with: luacheckrc_url: "" + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run unit tests + uses: lunarmodules/busted@v2.1.2 + with: + args: --verbose test.lua diff --git a/.gitignore b/.gitignore index 3d6528a..c18dd8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ __pycache__/ -test.lua diff --git a/README.md b/README.md index 988a537..a0faf87 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ all attributes are lowercase. While I try to reduce backwards incompatibilities, sometimes they are necessary to either fix bugs in formspec_ast or for implementing new formspec features. +#### April 2023 + + - The `current_tab` value of dropdowns is now parsed as a number. + #### February 2022 - The value of scrollbars is now a number instead of a string. diff --git a/elements.lua b/elements.lua index c0ceb28..b3071e1 100644 --- a/elements.lua +++ b/elements.lua @@ -54,7 +54,7 @@ a[47] = {{{"listelems", "string"}, "..."}} a[48] = {"selected_idx", "number"} a[49] = {"transparent", "boolean"} a[50] = {{{"captions", "string"}, "..."}} -a[51] = {"current_tab", "string"} +a[51] = {"current_tab", "number"} a[52] = {"draw_border", "boolean"} a[53] = {{{"items", "string"}, "..."}} a[54] = {"index_event", "boolean"} @@ -64,4 +64,4 @@ a[57] = {{a[56]}} a[58] = {{"props", "table"}, "..."} a[59] = {a[26]} a[60] = {{{{{"selectors", "string"}, "..."}}, a[58]}, {a[59], a[58]}} -return {["formspec_version"] = {{{"version", "number"}}}, ["size"] = {{a[3]}, {{a[1], a[2], {"fixed_size", "boolean"}}}}, ["position"] = a[5], ["anchor"] = a[5], ["padding"] = a[5], ["no_prepend"] = a[7], ["real_coordinates"] = {{{"bool", "boolean"}}}, ["container"] = a[5], ["container_end"] = a[7], ["scroll_container"] = {{a[4], a[3], a[8], a[9], {"scroll_factor", "number"}}, {a[4], a[3], a[8], a[9]}}, ["scroll_container_end"] = a[7], ["list"] = {{a[10], a[11], a[4], a[3], {"starting_item_index", "number"}}, {a[10], a[11], a[4], a[3]}}, ["listring"] = {{a[10], a[11]}, a[6]}, ["listcolors"] = {{a[12], a[13], a[14], {"tooltip_bgcolor", "string"}, {"tooltip_fontcolor", "string"}}, {a[12], a[13], a[14]}, {a[12], a[13]}}, ["tooltip"] = {{a[4], a[3], a[15], a[16], a[17]}, {a[4], a[3], a[15], a[16]}, {a[18], a[15], a[16], a[17]}, {a[4], a[3], a[15]}, {a[18], a[15], a[16]}, {a[18], a[15]}}, ["image"] = {{a[4], a[3], a[19], a[22]}, {a[4], a[3], a[19], a[23]}, {a[4], a[3], a[19], a[24]}, a[25]}, ["animated_image"] = {{a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[22]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[23]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[24]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29]}, {a[4], a[3], a[26], a[19], a[27], a[28]}}, ["model"] = {{a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34], a[35], {"animation_speed", "number"}}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34], a[35]}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34]}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33]}, {a[4], a[3], a[26], a[30], a[31], a[32]}, {a[4], a[3], a[26], a[30], a[31]}}, ["item_image"] = {{a[4], a[3], a[36]}}, ["bgcolor"] = {{a[16], a[37], {"fbgcolor", "string"}}, {a[16], a[37]}, {a[16]}}, ["background"] = {{a[4], a[3], a[19], a[38]}, a[25]}, ["background9"] = {{a[4], a[3], a[19], a[38], a[22]}, {a[4], a[3], a[19], a[38], a[23]}, {a[4], a[3], a[19], a[38], a[24]}}, ["pwdfield"] = a[40], ["field"] = {a[42], {a[26], a[39], a[41]}}, ["field_close_on_enter"] = {{a[26], {"close_on_enter", "boolean"}}}, ["textarea"] = {a[42]}, ["label"] = a[43], ["hypertext"] = {{a[4], a[3], a[26], {"text", "string"}}}, ["vertlabel"] = a[43], ["button"] = a[40], ["image_button"] = a[46], ["item_image_button"] = {{a[4], a[3], a[36], a[26], a[39]}}, ["button_exit"] = a[40], ["image_button_exit"] = a[46], ["textlist"] = {{a[4], a[3], a[26], a[47], a[48], a[49]}, {a[4], a[3], a[26], a[47], a[48]}, {a[4], a[3], a[26], a[47]}}, ["tabheader"] = {{a[4], a[2], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[3], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[2], a[26], a[50], a[51], a[49]}, {a[4], a[3], a[26], a[50], a[51], a[49]}, {a[4], a[26], a[50], a[51], a[49]}, {a[4], a[2], a[26], a[50], a[51]}, {a[4], a[3], a[26], a[50], a[51]}, {a[4], a[26], a[50], a[51]}}, ["box"] = {{a[4], a[3], {"color", "string"}}}, ["dropdown"] = {{a[4], a[3], a[26], a[53], a[48], a[54]}, {a[4], a[1], a[26], a[53], a[48], a[54]}, {a[4], a[3], a[26], a[53], a[48]}, {a[4], a[1], a[26], a[53], a[48]}}, ["checkbox"] = {{a[4], a[26], a[39], {"selected", "boolean"}}, {a[4], a[26], a[39]}}, ["scrollbar"] = {{a[4], a[3], a[9], a[26], a[55], {{"scrollbar_bg", "string"}, {"slider", "string"}, {"arrow_up", "string"}, {"arrow_down", "string"}}}, {a[4], a[3], a[9], a[26], a[55]}}, ["scrollbaroptions"] = a[57], ["table"] = {{a[4], a[3], a[26], {{{"cells", "string"}, "..."}}, a[48]}}, ["tableoptions"] = a[57], ["tablecolumns"] = {{{{{"type", "string"}, a[56]}, "..."}}}, ["style"] = a[60], ["style_type"] = a[60], ["set_focus"] = {{a[26], {"force", "boolean"}}, a[59]}} +return {["formspec_version"] = {{{"version", "number"}}}, ["size"] = {{a[3]}, {{a[1], a[2], {"fixed_size", "boolean"}}}}, ["position"] = a[5], ["anchor"] = a[5], ["padding"] = a[5], ["no_prepend"] = a[7], ["real_coordinates"] = {{{"bool", "boolean"}}}, ["container"] = a[5], ["container_end"] = a[7], ["scroll_container"] = {{a[4], a[3], a[8], a[9], {"scroll_factor", "number"}}, {a[4], a[3], a[8], a[9]}}, ["scroll_container_end"] = a[7], ["list"] = {{a[10], a[11], a[4], a[3], {"starting_item_index", "number"}}, {a[10], a[11], a[4], a[3]}}, ["listring"] = {{a[10], a[11]}, a[6]}, ["listcolors"] = {{a[12], a[13], a[14], {"tooltip_bgcolor", "string"}, {"tooltip_fontcolor", "string"}}, {a[12], a[13], a[14]}, {a[12], a[13]}}, ["tooltip"] = {{a[4], a[3], a[15], a[16], a[17]}, {a[4], a[3], a[15], a[16]}, {a[18], a[15], a[16], a[17]}, {a[4], a[3], a[15]}, {a[18], a[15], a[16]}, {a[18], a[15]}}, ["image"] = {{a[4], a[3], a[19], a[22]}, {a[4], a[3], a[19], a[23]}, {a[4], a[3], a[19], a[24]}, a[25]}, ["animated_image"] = {{a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[22]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[23]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29], a[24]}, {a[4], a[3], a[26], a[19], a[27], a[28], a[29]}, {a[4], a[3], a[26], a[19], a[27], a[28]}}, ["model"] = {{a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34], a[35], {"animation_speed", "number"}}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34], a[35]}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33], a[34]}, {a[4], a[3], a[26], a[30], a[31], a[32], a[33]}, {a[4], a[3], a[26], a[30], a[31], a[32]}, {a[4], a[3], a[26], a[30], a[31]}}, ["item_image"] = {{a[4], a[3], a[36]}}, ["bgcolor"] = {{a[16], a[37], {"fbgcolor", "string"}}, {a[16], a[37]}, {a[16]}}, ["background"] = {{a[4], a[3], a[19], a[38]}, a[25]}, ["background9"] = {{a[4], a[3], a[19], a[38], a[22]}, {a[4], a[3], a[19], a[38], a[23]}, {a[4], a[3], a[19], a[38], a[24]}}, ["pwdfield"] = a[40], ["field"] = {a[42], {a[26], a[39], a[41]}}, ["field_close_on_enter"] = {{a[26], {"close_on_enter", "boolean"}}}, ["textarea"] = {a[42]}, ["label"] = a[43], ["hypertext"] = {{a[4], a[3], a[26], {"text", "string"}}}, ["vertlabel"] = a[43], ["button"] = a[40], ["image_button"] = a[46], ["item_image_button"] = {{a[4], a[3], a[36], a[26], a[39]}}, ["button_exit"] = a[40], ["image_button_exit"] = a[46], ["textlist"] = {{a[4], a[3], a[26], a[47], a[48], a[49]}, {a[4], a[3], a[26], a[47], a[48]}, {a[4], a[3], a[26], a[47]}}, ["tabheader"] = {{a[4], a[2], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[3], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[26], a[50], a[51], a[49], a[52]}, {a[4], a[26], a[50], a[51]}}, ["box"] = {{a[4], a[3], {"color", "string"}}}, ["dropdown"] = {{a[4], a[3], a[26], a[53], a[48], a[54]}, {a[4], a[1], a[26], a[53], a[48], a[54]}, {a[4], a[3], a[26], a[53], a[48]}, {a[4], a[1], a[26], a[53], a[48]}}, ["checkbox"] = {{a[4], a[26], a[39], {"selected", "boolean"}}, {a[4], a[26], a[39]}}, ["scrollbar"] = {{a[4], a[3], a[9], a[26], a[55], {{"scrollbar_bg", "string"}, {"slider", "string"}, {"arrow_up", "string"}, {"arrow_down", "string"}}}, {a[4], a[3], a[9], a[26], a[55]}}, ["scrollbaroptions"] = a[57], ["table"] = {{a[4], a[3], a[26], {{{"cells", "string"}, "..."}}, a[48]}}, ["tableoptions"] = a[57], ["tablecolumns"] = {{{{{"type", "string"}, a[56]}, "..."}}}, ["style"] = a[60], ["style_type"] = a[60], ["set_focus"] = {{a[26], {"force", "boolean"}}, a[59]}} diff --git a/elements.yaml b/elements.yaml index 152686e..9be863f 100644 --- a/elements.yaml +++ b/elements.yaml @@ -483,7 +483,7 @@ tabheader: - [name, string] - - - [captions, string] - '...' - - [current_tab, string] + - [current_tab, number] - [transparent, boolean] - [draw_border, boolean] - - - [x, number] @@ -493,7 +493,7 @@ tabheader: - [name, string] - - - [captions, string] - '...' - - [current_tab, string] + - [current_tab, number] - [transparent, boolean] - [draw_border, boolean] - - - [x, number] @@ -501,54 +501,15 @@ tabheader: - [name, string] - - - [captions, string] - '...' - - [current_tab, string] + - [current_tab, number] - [transparent, boolean] - [draw_border, boolean] - - - [x, number] - [y, number] - - [h, number] - [name, string] - - - [captions, string] - '...' - - [current_tab, string] - - [transparent, boolean] -- - - [x, number] - - [y, number] - - - [w, number] - - [h, number] - - [name, string] - - - - [captions, string] - - '...' - - [current_tab, string] - - [transparent, boolean] -- - - [x, number] - - [y, number] - - [name, string] - - - - [captions, string] - - '...' - - [current_tab, string] - - [transparent, boolean] -- - - [x, number] - - [y, number] - - [h, number] - - [name, string] - - - - [captions, string] - - '...' - - [current_tab, string] -- - - [x, number] - - [y, number] - - - [w, number] - - [h, number] - - [name, string] - - - - [captions, string] - - '...' - - [current_tab, string] -- - - [x, number] - - [y, number] - - [name, string] - - - - [captions, string] - - '...' - - [current_tab, string] + - [current_tab, number] table: - - - [x, number] - [y, number] diff --git a/make_elements.py b/make_elements.py index 123b852..d9059c5 100755 --- a/make_elements.py +++ b/make_elements.py @@ -14,7 +14,7 @@ def _make_known(**kwargs): return known _known = _make_known( - number=('x', 'y', 'w', 'h', 'selected_idx', 'version', + number=('x', 'y', 'w', 'h', 'selected_idx', 'version', 'current_tab', 'starting_item_index', 'scroll_factor', 'frame_count', 'frame_duration', 'frame_start', 'animation_speed', 'value'), boolean=('auto_clip', 'fixed_size', 'transparent', 'draw_border', 'bool', @@ -193,6 +193,19 @@ def _image_button_hook(params): params.append(('pressed_texture_name', 'string')) yield params + +# Work around tabheader's documentation +@hook('tabheader') +def _tabheader_hook(params): + yield params + if len(params) == 6: + assert params[4:] == [ + ('transparent', 'boolean'), + ('draw_border', 'boolean'), + ] + yield params[:4] + + # Support MultiCraft's non-standard scrollbar styling # WARNING: This may be removed or broken without notice @hook('scrollbar') @@ -283,7 +296,7 @@ def parse(data): return res -URL = 'https://github.com/minetest/minetest/raw/master/doc/lua_api.txt' +URL = 'https://github.com/minetest/minetest/raw/master/doc/lua_api.md' def fetch_and_parse(*, url=URL): with urllib.request.urlopen(url) as f: raw = f.read() diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..8ffe6dd --- /dev/null +++ b/test.lua @@ -0,0 +1,619 @@ +dofile('init.lua') + +local function test_parse(fs, expected_tree) + -- Make single elements lists and add formspec_version + if expected_tree.type then + expected_tree = {expected_tree} + end + if not expected_tree.formspec_version then + expected_tree.formspec_version = 1 + end + + local tree = assert(formspec_ast.parse(fs)) + assert.same(tree, expected_tree) +end + +local function test_parse_unparse(fs, expected_tree) + test_parse(fs, expected_tree) + local unparsed_fs = assert(formspec_ast.unparse(expected_tree)) + assert.equals(fs, unparsed_fs) +end + +it("can parse complex escapes", function() + test_parse_unparse([=[label[123,456;yay abc def\, ghi\; jkl mno \]\\]]=], { + formspec_version = 1, + { + type = 'label', + x = 123, + y = 456, + label = 'yay abc def, ghi; jkl mno ]\\' + } + }) +end) + +it("can round-trip most elements", function() + local fs = [[ + formspec_version[2] + size[5,2] + padding[1,2] + no_prepend[] + container[1,1] + label[0,0;Containers are fun\]\\] + container[-1,-1] + button[0.5,0;4,1;name;Label] + container_end[] + label[0,1;Nested containers work too.] + scroll_container[0,2;1,1;scrollbar;vertical] + button[0.5,0;4,1;name;Label] + scroll_container_end[] + container_end[] + image[0,1;1,1;air.png] + set_focus[name;true] + dropdown[0,0;1;test;abc,def,ghi,jkl;2] + dropdown[0,0;1;test;abc,def,ghi,jkl;2;true] + field_close_on_enter[my-field;false] + bgcolor[blue] + bgcolor[blue;true] + bgcolor[blue;both;green] + tooltip[1,2;3,4;text] + tooltip[elem;text;bgcolor] + background9[1,2;3,4;bg.png;false;5] + background9[1,2;3,4;bg.png;false;5,6] + background9[1,2;3,4;bg.png;false;5,6,7,8] + tablecolumns[text;image;color,option=value;tree] + list[test;test2;1,2;3,4;5] + list[test6;test7;8,9;10,11] + image_button[1,2;3,4;img.png;name;label] + image_button[1,2;3,4;img.png;name;label;false;true] + image_button[1,2;3,4;img.png;name;label;true;false;img2.png] + image_button_exit[1,2;3,4;img.png;name;label] + image_button_exit[1,2;3,4;img.png;name;label;false;true] + image_button_exit[1,2;3,4;img.png;name;label;true;false;img2.png] + image[1,2;3,4;air.png;5] + image[1,2;3,4;air.png;5,6] + image[1,2;3,4;air.png;5,6,7,8] + ]] + fs = ('\n' .. fs):gsub('\n[ \n]*', '') + + test_parse_unparse(fs, { + formspec_version = 2, + { + type = "size", + w = 5, + h = 2, + }, + { + type = "padding", + x = 1, + y = 2, + }, + { + type = "no_prepend" + }, + { + type = "container", + x = 1, + y = 1, + { + type = "label", + x = 0, + y = 0, + label = "Containers are fun]\\", + }, + { + type = "container", + x = -1, + y = -1, + { + type = "button", + x = 0.5, + y = 0, + w = 4, + h = 1, + name = "name", + label = "Label", + }, + }, + { + type = "label", + x = 0, + y = 1, + label = "Nested containers work too.", + }, + { + type = "scroll_container", + x = 0, + y = 2, + w = 1, + h = 1, + scrollbar_name = "scrollbar", + orientation = "vertical", + -- scroll_factor = nil, + { + h = 1, + y = 0, + label = "Label", + w = 4, + name = "name", + x = 0.5, + type = "button" + }, + }, + }, + { + type = "image", + x = 0, + y = 1, + w = 1, + h = 1, + texture_name = "air.png", + }, + { + type = "set_focus", + name = "name", + force = true, + }, + { + type = "dropdown", + x = 0, + y = 0, + w = 1, + name = "test", + items = {"abc", "def", "ghi", "jkl"}, + selected_idx = 2, + }, + { + type = "dropdown", + x = 0, + y = 0, + w = 1, + name = "test", + items = {"abc", "def", "ghi", "jkl"}, + selected_idx = 2, + index_event = true, + }, + { + type = "field_close_on_enter", + name = "my-field", + close_on_enter = false, + }, + { + type = "bgcolor", + bgcolor = "blue", + }, + { + type = "bgcolor", + bgcolor = "blue", + fullscreen = true, + }, + { + type = "bgcolor", + bgcolor = "blue", + fullscreen = "both", + fbgcolor = "green", + }, + { + type = "tooltip", + x = 1, + y = 2, + w = 3, + h = 4, + tooltip_text = "text", + }, + { + type = "tooltip", + gui_element_name = "elem", + tooltip_text = "text", + bgcolor = "bgcolor", + }, + { + type = "background9", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "bg.png", + auto_clip = false, + middle_x = 5, + }, + { + type = "background9", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "bg.png", + auto_clip = false, + middle_x = 5, + middle_y = 6, + }, + { + type = "background9", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "bg.png", + auto_clip = false, + middle_x = 5, + middle_y = 6, + middle_x2 = 7, + middle_y2 = 8, + }, + { + type = "tablecolumns", + tablecolumns = { + {type = "text", opts = {}}, + {type = "image", opts = {}}, + {type = "color", opts = {option = "value"}}, + {type = "tree", opts = {}}, + }, + }, + { + type = "list", + inventory_location = "test", + list_name = "test2", + x = 1, + y = 2, + w = 3, + h = 4, + starting_item_index = 5, + }, + { + type = "list", + inventory_location = "test6", + list_name = "test7", + x = 8, + y = 9, + w = 10, + h = 11, + }, + { + type = "image_button", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + }, + { + type = "image_button", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + noclip = false, + drawborder = true, + }, + { + type = "image_button", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + noclip = true, + drawborder = false, + pressed_texture_name = "img2.png", + }, + { + type = "image_button_exit", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + }, + { + type = "image_button_exit", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + noclip = false, + drawborder = true, + }, + { + type = "image_button_exit", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "img.png", + name = "name", + label = "label", + noclip = true, + drawborder = false, + pressed_texture_name = "img2.png", + }, + { + type = "image", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "air.png", + middle_x = 5, + }, + { + type = "image", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "air.png", + middle_x = 5, + middle_y = 6, + }, + { + type = "image", + x = 1, + y = 2, + w = 3, + h = 4, + texture_name = "air.png", + middle_x = 5, + middle_y = 6, + middle_x2 = 7, + middle_y2 = 8, + }, + }) +end) + +local function remove_trailing_params(elem_s, elem, ...) + local res = {} + local strings = {} + local optional_params = {...} + for i = #optional_params, 1, -1 do + local p = optional_params[i] + local no_copy = {} + if type(p) == "table" then + for _, param in ipairs(p) do + no_copy[param] = true + end + else + no_copy[p] = true + end + + res[i] = elem + strings[i] = elem_s + elem_s = elem_s:gsub(";[^;]+%]$", "]") + local old_elem = elem + elem = {} + for k, v in pairs(old_elem) do + if not no_copy[k] then + elem[k] = v + end + end + end + return table.concat(strings, ""), res +end + +it("can parse model[]", function() + test_parse_unparse(remove_trailing_params( + "model[1,2;3,4;abc;def;ghi,jkl;5,6;true;false;7,8;9]", + { + type = "model", + x = 1, + y = 2, + w = 3, + h = 4, + name = "abc", + mesh = "def", + textures = {"ghi", "jkl"}, + rotation_x = 5, + rotation_y = 6, + continuous = true, + mouse_control = false, + frame_loop_begin = 7, + frame_loop_end = 8, + animation_speed = 9 + }, + {"rotation_x", "rotation_y"}, "continuous", "mouse_control", + {"frame_loop_begin", "frame_loop_end"}, "animation_speed" + )) +end) + +it("can round-trip style[]", function() + local s = 'style[test1,test2;def=ghi]style_type[test;abc=def]' + assert.equals(s, assert(formspec_ast.interpret(s))) + test_parse('style[name,name2;bgcolor=blue;textcolor=yellow]', { + type = "style", + selectors = { + "name", + "name2", + }, + props = { + bgcolor = "blue", + textcolor = "yellow", + }, + }) +end) + +-- Test item/items compatibility +it("can parse dropdown[]", function() + local expected = 'dropdown[0,0;1,2;test;abc,def,ghi,jkl;2;true]' + assert.equals( + expected, + assert(formspec_ast.unparse({ + { + type = "dropdown", + x = 0, + y = 0, + w = 1, + h = 2, + name = "test", + items = {"abc", "def", "ghi", "jkl"}, + selected_idx = 2, + index_event = true, + }, + })) + ) + assert.equals( + expected, + assert(formspec_ast.unparse({ + { + type = "dropdown", + x = 0, + y = 0, + w = 1, + h = 2, + name = "test", + item = {"abc", "def", "ghi", "jkl"}, + selected_idx = 2, + index_event = true, + }, + })) + ) +end) + +-- Ensure the style[] unparse compatibility works correctly +it("can unparse style with name", function() + local expected = 'style_type[test;abc=def]' + assert.equals( + expected, + assert(formspec_ast.unparse({ + { + type = 'style_type', + name = 'test', + props = { + abc = 'def', + }, + } + })) + ) + assert.equals( + expected, + assert(formspec_ast.unparse({ + { + type = 'style_type', + selectors = { + 'test', + }, + props = { + abc = 'def', + }, + } + })) + ) +end) + +-- Ensure flatten works correctly +it("flattens formspecs correctly", function() + assert.equals( + 'label[0,0;abc]label[2,2;def]scroll_container[1,1;2,2;test;vertical]' .. + 'image[1,1;1,1;def]scroll_container_end[]', + formspec_ast.unparse(formspec_ast.flatten(assert(formspec_ast.parse([[ + label[0,0;abc] + container[3,2] + container[-1,0] + label[0,0;def] + container_end[] + container_end[] + scroll_container[1,1;2,2;test;vertical] + image[1,1;1,1;def] + scroll_container_end[] + ]])))) + ) +end) + +it("interprets invsize[] and escapes correctly", function() + assert.equals(assert(formspec_ast.interpret('invsize[12,34]')), 'size[12,34]') + + assert.equals(assert(formspec_ast.interpret('label[1,2;abc\\')), + 'label[1,2;abc]') + assert.equals(assert(formspec_ast.interpret('label[1,2;abc\\\\')), + 'label[1,2;abc\\\\]') + + assert.equals(formspec_ast.formspec_escape('label[1,2;abc\\def]'), + 'label\\[1\\,2\\;abc\\\\def\\]') +end) + +it("safe_interpret", function() + assert.equals( + formspec_ast.safe_interpret([[ + formspec_version[5.1] + size[3,3] + label[0,0;Hi] + image[1,2;3,4;a^b\[c] + formspec_ast:crash[] + textlist[1,2;3,4;test;a,b,c] + ]]), + 'formspec_version[5]size[3,3]label[0,0;Hi]image[1,2;3,4;a^b]' .. + 'textlist[1,2;3,4;test;a,b,c]' + ) +end) + +it("can parse tabheader", function() + local fs = + "tabheader[1,2;name;a,b,c;1]" .. + "tabheader[1,2;name;a,b,c;1;false;true]" .. + + -- Width and height can only be specified if transparent and drawborder + -- are specified + "tabheader[1,2;3,4;name;a,b,c;1;false;true]" .. + "tabheader[1,2;3;name;a,b,c;1;false;true]" .. + "tabheader[1,2;3;name;a,b,c;1;;]" + + test_parse_unparse(fs, { + { + type = "tabheader", + x = 1, y = 2, + name = "name", + captions = {"a", "b", "c"}, + current_tab = 1 + }, + { + type = "tabheader", + x = 1, y = 2, + name = "name", + captions = {"a", "b", "c"}, + current_tab = 1, + transparent = false, + draw_border = true + }, + { + type = "tabheader", + x = 1, y = 2, w = 3, h = 4, + name = "name", + captions = {"a", "b", "c"}, + current_tab = 1, + transparent = false, + draw_border = true + }, + { + type = "tabheader", + x = 1, y = 2, h = 3, + name = "name", + captions = {"a", "b", "c"}, + current_tab = 1, + transparent = false, + draw_border = true + }, + { + type = "tabheader", + x = 1, y = 2, h = 3, + name = "name", + captions = {"a", "b", "c"}, + current_tab = 1, + }, + }) +end) + +it("does not parse invalid tabheader elements", function() + assert.is_nil(formspec_ast.parse("tabheader[1,2;name;a,b,c]")) + assert.is_nil(formspec_ast.parse("tabheader[1,2;name;a,b,c;1;false]")) + assert.is_nil(formspec_ast.parse("tabheader[1,2;3,4;name;a,b,c;1]")) +end) diff --git a/tests.lua b/tests.lua deleted file mode 100644 index 356bbc1..0000000 --- a/tests.lua +++ /dev/null @@ -1,581 +0,0 @@ -dofile('init.lua') - -local function dump(obj) - if type(obj) == 'string' then - return ('%q'):format(obj) - elseif type(obj) == 'table' then - local entries = {} - for k, v in pairs(obj) do - if type(k) == 'string' and k:match('^[a-zA-Z_][a-zA-Z0-9_]*$') then - entries[#entries + 1] = k .. ' = ' .. dump(v) - else - entries[#entries + 1] = '[' .. dump(k) .. '] = ' .. dump(v) - end - end - table.sort(entries) - return '{' .. table.concat(entries, ', ') .. '}' - end - return tostring(obj) -end - -local function equal(t1, t2) - if type(t1) ~= 'table' or type(t2) ~= 'table' then - return t1 == t2 - end - for k, v in pairs(t1) do - if not equal(v, t2[k]) then - print(k, v, dump(t1), dump(t2)) - return false - end - end - for k in pairs(t2) do - if t1[k] == nil then - return false - end - end - return true -end - -local function assert_equal(obj1, ...) - for i = 1, select('#', ...) do - local objn = select(i, ...) - if not equal(obj1, objn) then - error(('%s ~= %s'):format(obj1, objn)) - end - end -end - -local function test_parse(fs, expected_tree) - -- Make single elements lists and add formspec_version - if expected_tree.type then - expected_tree = {expected_tree} - end - if not expected_tree.formspec_version then - expected_tree.formspec_version = 1 - end - - local tree = assert(formspec_ast.parse(fs)) - assert_equal(tree, expected_tree) -end - -local function test_parse_unparse(fs, expected_tree) - test_parse(fs, expected_tree) - local unparsed_fs = assert(formspec_ast.unparse(expected_tree)) - assert_equal(fs, unparsed_fs) -end - -test_parse_unparse([=[label[123,456;yay abc def\, ghi\; jkl mno \]\\]]=], { - formspec_version = 1, - { - type = 'label', - x = 123, - y = 456, - label = 'yay abc def, ghi; jkl mno ]\\' - } -}) - -local fs = [[ - formspec_version[2] - size[5,2] - padding[1,2] - no_prepend[] - container[1,1] - label[0,0;Containers are fun\]\\] - container[-1,-1] - button[0.5,0;4,1;name;Label] - container_end[] - label[0,1;Nested containers work too.] - scroll_container[0,2;1,1;scrollbar;vertical] - button[0.5,0;4,1;name;Label] - scroll_container_end[] - container_end[] - image[0,1;1,1;air.png] - set_focus[name;true] - dropdown[0,0;1;test;abc,def,ghi,jkl;2] - dropdown[0,0;1;test;abc,def,ghi,jkl;2;true] - field_close_on_enter[my-field;false] - bgcolor[blue] - bgcolor[blue;true] - bgcolor[blue;both;green] - tooltip[1,2;3,4;text] - tooltip[elem;text;bgcolor] - background9[1,2;3,4;bg.png;false;5] - background9[1,2;3,4;bg.png;false;5,6] - background9[1,2;3,4;bg.png;false;5,6,7,8] - tablecolumns[text;image;color,option=value;tree] - list[test;test2;1,2;3,4;5] - list[test6;test7;8,9;10,11] - image_button[1,2;3,4;img.png;name;label] - image_button[1,2;3,4;img.png;name;label;false;true] - image_button[1,2;3,4;img.png;name;label;true;false;img2.png] - image_button_exit[1,2;3,4;img.png;name;label] - image_button_exit[1,2;3,4;img.png;name;label;false;true] - image_button_exit[1,2;3,4;img.png;name;label;true;false;img2.png] - image[1,2;3,4;air.png;5] - image[1,2;3,4;air.png;5,6] - image[1,2;3,4;air.png;5,6,7,8] -]] -fs = ('\n' .. fs):gsub('\n[ \n]*', '') - -test_parse_unparse(fs, { - formspec_version = 2, - { - type = "size", - w = 5, - h = 2, - }, - { - type = "padding", - x = 1, - y = 2, - }, - { - type = "no_prepend" - }, - { - type = "container", - x = 1, - y = 1, - { - type = "label", - x = 0, - y = 0, - label = "Containers are fun]\\", - }, - { - type = "container", - x = -1, - y = -1, - { - type = "button", - x = 0.5, - y = 0, - w = 4, - h = 1, - name = "name", - label = "Label", - }, - }, - { - type = "label", - x = 0, - y = 1, - label = "Nested containers work too.", - }, - { - type = "scroll_container", - x = 0, - y = 2, - w = 1, - h = 1, - scrollbar_name = "scrollbar", - orientation = "vertical", - -- scroll_factor = nil, - { - h = 1, - y = 0, - label = "Label", - w = 4, - name = "name", - x = 0.5, - type = "button" - }, - }, - }, - { - type = "image", - x = 0, - y = 1, - w = 1, - h = 1, - texture_name = "air.png", - }, - { - type = "set_focus", - name = "name", - force = true, - }, - { - type = "dropdown", - x = 0, - y = 0, - w = 1, - name = "test", - items = {"abc", "def", "ghi", "jkl"}, - selected_idx = 2, - }, - { - type = "dropdown", - x = 0, - y = 0, - w = 1, - name = "test", - items = {"abc", "def", "ghi", "jkl"}, - selected_idx = 2, - index_event = true, - }, - { - type = "field_close_on_enter", - name = "my-field", - close_on_enter = false, - }, - { - type = "bgcolor", - bgcolor = "blue", - }, - { - type = "bgcolor", - bgcolor = "blue", - fullscreen = true, - }, - { - type = "bgcolor", - bgcolor = "blue", - fullscreen = "both", - fbgcolor = "green", - }, - { - type = "tooltip", - x = 1, - y = 2, - w = 3, - h = 4, - tooltip_text = "text", - }, - { - type = "tooltip", - gui_element_name = "elem", - tooltip_text = "text", - bgcolor = "bgcolor", - }, - { - type = "background9", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "bg.png", - auto_clip = false, - middle_x = 5, - }, - { - type = "background9", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "bg.png", - auto_clip = false, - middle_x = 5, - middle_y = 6, - }, - { - type = "background9", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "bg.png", - auto_clip = false, - middle_x = 5, - middle_y = 6, - middle_x2 = 7, - middle_y2 = 8, - }, - { - type = "tablecolumns", - tablecolumns = { - {type = "text", opts = {}}, - {type = "image", opts = {}}, - {type = "color", opts = {option = "value"}}, - {type = "tree", opts = {}}, - }, - }, - { - type = "list", - inventory_location = "test", - list_name = "test2", - x = 1, - y = 2, - w = 3, - h = 4, - starting_item_index = 5, - }, - { - type = "list", - inventory_location = "test6", - list_name = "test7", - x = 8, - y = 9, - w = 10, - h = 11, - }, - { - type = "image_button", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - }, - { - type = "image_button", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - noclip = false, - drawborder = true, - }, - { - type = "image_button", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - noclip = true, - drawborder = false, - pressed_texture_name = "img2.png", - }, - { - type = "image_button_exit", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - }, - { - type = "image_button_exit", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - noclip = false, - drawborder = true, - }, - { - type = "image_button_exit", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "img.png", - name = "name", - label = "label", - noclip = true, - drawborder = false, - pressed_texture_name = "img2.png", - }, - { - type = "image", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "air.png", - middle_x = 5, - }, - { - type = "image", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "air.png", - middle_x = 5, - middle_y = 6, - }, - { - type = "image", - x = 1, - y = 2, - w = 3, - h = 4, - texture_name = "air.png", - middle_x = 5, - middle_y = 6, - middle_x2 = 7, - middle_y2 = 8, - }, -}) - -local function remove_trailing_params(elem_s, elem, ...) - local res = {} - local strings = {} - local optional_params = {...} - for i = #optional_params, 1, -1 do - local p = optional_params[i] - local no_copy = {} - if type(p) == "table" then - for _, param in ipairs(p) do - no_copy[param] = true - end - else - no_copy[p] = true - end - - res[i] = elem - strings[i] = elem_s - elem_s = elem_s:gsub(";[^;]+%]$", "]") - local old_elem = elem - elem = {} - for k, v in pairs(old_elem) do - if not no_copy[k] then - elem[k] = v - end - end - end - return table.concat(strings, ""), res -end - -test_parse_unparse(remove_trailing_params( - "model[1,2;3,4;abc;def;ghi,jkl;5,6;true;false;7,8;9]", - { - type = "model", - x = 1, - y = 2, - w = 3, - h = 4, - name = "abc", - mesh = "def", - textures = {"ghi", "jkl"}, - rotation_x = 5, - rotation_y = 6, - continuous = true, - mouse_control = false, - frame_loop_begin = 7, - frame_loop_end = 8, - animation_speed = 9 - }, - {"rotation_x", "rotation_y"}, "continuous", "mouse_control", - {"frame_loop_begin", "frame_loop_end"}, "animation_speed" -)) - - --- Make sure style[] (un)parses correctly -local s = 'style[test1,test2;def=ghi]style_type[test;abc=def]' -assert_equal(s, assert(formspec_ast.interpret(s))) -test_parse('style[name,name2;bgcolor=blue;textcolor=yellow]', { - type = "style", - selectors = { - "name", - "name2", - }, - props = { - bgcolor = "blue", - textcolor = "yellow", - }, -}) - --- Test item/items compatibility -assert_equal( - 'dropdown[0,0;1,2;test;abc,def,ghi,jkl;2;true]', - assert(formspec_ast.unparse({ - { - type = "dropdown", - x = 0, - y = 0, - w = 1, - h = 2, - name = "test", - items = {"abc", "def", "ghi", "jkl"}, - selected_idx = 2, - index_event = true, - }, - })), - assert(formspec_ast.unparse({ - { - type = "dropdown", - x = 0, - y = 0, - w = 1, - h = 2, - name = "test", - item = {"abc", "def", "ghi", "jkl"}, - selected_idx = 2, - index_event = true, - }, - })) -) - --- Ensure the style[] unparse compatibility works correctly -assert_equal( - 'style_type[test;abc=def]', - assert(formspec_ast.unparse({ - { - type = 'style_type', - name = 'test', - props = { - abc = 'def', - }, - } - })), - assert(formspec_ast.unparse({ - { - type = 'style_type', - selectors = { - 'test', - }, - props = { - abc = 'def', - }, - } - })) -) - --- Ensure flatten works correctly -assert_equal( - 'label[0,0;abc]label[2,2;def]scroll_container[1,1;2,2;test;vertical]' .. - 'image[1,1;1,1;def]scroll_container_end[]', - formspec_ast.unparse(formspec_ast.flatten(assert(formspec_ast.parse([[ - label[0,0;abc] - container[3,2] - container[-1,0] - label[0,0;def] - container_end[] - container_end[] - scroll_container[1,1;2,2;test;vertical] - image[1,1;1,1;def] - scroll_container_end[] - ]])))) -) - --- Ensure invsize[] is converted to size[] -assert_equal(assert(formspec_ast.interpret('invsize[12,34]')), 'size[12,34]') - -assert_equal(assert(formspec_ast.interpret('label[1,2;abc\\')), - 'label[1,2;abc]') -assert_equal(assert(formspec_ast.interpret('label[1,2;abc\\\\')), - 'label[1,2;abc\\\\]') - -assert_equal(formspec_ast.formspec_escape('label[1,2;abc\\def]'), - 'label\\[1\\,2\\;abc\\\\def\\]') - -assert_equal( - formspec_ast.safe_interpret([[ - formspec_version[5.1] - size[3,3] - label[0,0;Hi] - image[1,2;3,4;a^b\[c] - formspec_ast:crash[] - textlist[1,2;3,4;test;a,b,c] - ]]), - 'formspec_version[5]size[3,3]label[0,0;Hi]image[1,2;3,4;a^b]' .. - 'textlist[1,2;3,4;test;a,b,c]' -) - -print('Tests pass')