2021-05-29 01:43:09 +02:00
--- Uncomment these 2 lines to run in standalone mode
2021-05-29 01:44:17 +02:00
-- worldeditadditions = { parse = { } }
2021-05-29 01:43:09 +02:00
-- function worldeditadditions.trim(str) return (str:gsub("^%s*(.-)%s*$", "%1")) end
2021-05-29 01:54:30 +02:00
--- The main tokeniser. Splits the input string up into space separated tokens, except when said spaces are inside { curly braces }.
-- Note that the outermost set of curly braces are stripped.
-- @param str string The input string to tokenise.
-- @returns string[] A list of tokens
2021-05-29 01:43:09 +02:00
local function tokenise ( str )
2021-05-29 01:54:30 +02:00
if type ( str ) ~= " string " then return false , " Error: Expected input of type string. " end
2021-05-29 01:43:09 +02:00
str = str : gsub ( " %s+ " , " " ) -- Replace all runs of whitespace with a single space
2021-05-29 01:54:30 +02:00
-- The resulting tokens
local result = { }
2021-05-29 01:43:09 +02:00
local nested_depth = 0 -- The nested depth inside { and } we're currently at
local nested_stack = { } -- Stack of starting positions of curly brace { } blocks
local scanpos = 1 -- The current position we're scanning
while scanpos <= # str do
-- Find the next character of interest
local nextpos = str : find ( " [%s{}] " , scanpos )
-- If it's nil, then cleanup and return
if nextpos == nil then
if nested_depth > 0 then
-- Handle unclosed brace groups
return false , " Error: Unclosed brace group detected. "
else
-- Make sure we catch any trailing parts
local str_trailing = str : sub ( scanpos )
if # str_trailing then table.insert ( result , str_trailing ) end
return true , result
end
end
-- Extract the character in question
local char = str : sub ( nextpos , nextpos )
if char == " } " then
2021-12-31 14:16:09 +01:00
if nested_depth > 0 then
-- Decrease the nested depth
nested_depth = nested_depth - 1
-- Pop the start of this block off the stack and find this block's contents
local block_start = table.remove ( nested_stack , # nested_stack )
local substr = str : sub ( block_start , nextpos - 1 )
if # substr > 0 and nested_depth == 0 then table.insert ( result , substr ) end
end
2021-05-29 01:43:09 +02:00
elseif char == " { " then
-- Increase the nested depth, and store this position on the stack for later
nested_depth = nested_depth + 1
table.insert ( nested_stack , nextpos + 1 )
else
-- It's a space! Extract a part, but only if the nested depth is 0 (i.e. we're not inside any braces).
local substr = str : sub ( scanpos , nextpos - 1 )
if # substr > 0 and nested_depth == 0 then table.insert ( result , substr ) end
end
-- Move the scanning position up to just after the character we've just
-- found and handled
scanpos = nextpos + 1
end
-- Handle any trailing bits
local str_trailing = str : sub ( scanpos )
if # str_trailing > 0 then table.insert ( result , str_trailing ) end
return true , result
end
2021-05-29 01:54:30 +02:00
--- Recombines a list of tokens into a list of commands.
-- @param parts string[] The tokens from tokenise(str).
-- @returns string[] The tokens, but run through trim() & grouped into commands (1 element in the list = 1 command)
2021-05-29 01:43:09 +02:00
local function recombine ( parts )
local result = { }
local acc = { }
for i , value in ipairs ( parts ) do
value = worldeditadditions.trim ( value )
if value : sub ( 1 , 1 ) == " / " and # acc > 0 then
table.insert ( result , table.concat ( acc , " " ) )
acc = { }
end
table.insert ( acc , value )
end
if # acc > 0 then table.insert ( result , table.concat ( acc , " " ) ) end
return result
end
2021-05-29 01:57:42 +02:00
--- Tokenises a string of multiple commands into an array of individual commands.
-- Preserves the forward slash at the beginning of each command name.
-- Also supports arbitrarily nested and complex curly braces { } for grouping
-- commands together that would normally be split apart.
--
-- Simple example:
-- INPUT: //1 //2 //outset 25 //fixlight
-- OUTPUT: { "//1", "//2", "//outset 25", "//fixlight" }
--
-- Example with curly braces:
-- INPUT: //1 //2 //outset 50 {//many 5 //multi //fixlight //clearcut}
-- OUTPUT: { "//1", "//2", "//outset 50", "//many 5 //multi //fixlight //clearcut"}
--
-- @param command_str str The command string to operate on.
-- @returns bool,(string[]|string) If the operation was successful, then true followed by a table of strings is returned. If the operation was not successful, then false followed by an error message (as a single string) is returned instead.
function worldeditadditions . parse . tokenise_commands ( command_str )
local success , result = tokenise ( command_str )
if not success then return success , result end
return true , recombine ( result )
end
2021-05-29 01:43:09 +02:00
----- Test harness code -----
-----------------------------
-- local function printparts(tbl)
-- for key,value in ipairs(tbl) do
-- print(key..": "..value)
-- end
-- end
--
-- local function test_input(input)
2021-05-29 01:44:17 +02:00
-- local success, result = worldeditadditions.parse.tokenise_commands(input)
2021-05-29 01:43:09 +02:00
-- if success then
-- printparts(result)
--
-- -- print("RECOMBINED:")
-- -- printparts(recombine(result))
-- else
-- print(result)
-- end
--
-- end
--
-- print("\n\n\n*** 1 ***")
-- test_input("//multi //1 //cubeapply 10 set dirt")
-- print("\n\n\n*** 2 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //fixlight //clearcut}")
-- print("\n\n\n*** 3 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //ellipsoid 10 5 7 glass //clearcut}")
-- print("\n\n\n*** 4 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //ellipsoid 10 5 7 glass //clearcut //many {//set dirt //fixlight}}")
-- print("\n\n\n*** 5 ***")
-- test_input("a { b c d { e f { g h i }j} k l m n}o p")
-- print("\n\n\n*** 6 ***")
-- test_input("a { b c d } e f {{ g h i }j k l m n}o p")