This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,5 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/string-utilities"
version = "1.0.0"
commit = "7cbef7885ca023d71dd93f02ffa32cbda65faa9c"
source = "url+https://github.com/roblox/string-utilities"
@@ -0,0 +1,84 @@
--!nocheck
local RunService = game:GetService("RunService")
local StringSplit = require(script.Parent.StringSplit)
function assertIsType(value, expectedType, name)
if RunService:IsStudio() or _G.__TESTEZ_RUNNING_TEST__ then
assert(
typeof(value) == expectedType,
string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value))
)
end
end
local function urlDecode(input)
assertIsType(input, "string", "input")
local decoded = string.gsub(input, '%%(%x%x)', function(charCode)
return string.char(tonumber(charCode, 16))
end)
decoded = string.gsub(decoded, "+", " ")
return decoded;
end
--[[
Parses a query string into a lua table.
* supports both "&"" and ";" as separator (and "=" separates names/values)
* empty names or values are returned as "" (eg "k1=&=v2&k3")
* completely empty pairs are ignored (eg "k1=v1&&k3=v3")
* url decodes all names and values
* supports multiple values
* by default, all values are returned in tables
* if listKeyMapper (name => listKey) is provided:
* only the last value for a name is returned as that key (ie name)
* the list of values is returned at the key provided by listKeyMapper
* listKeyMapper can return the same input key (ie name), or nil (same effect)
]]
local function ParseQuery(input, listKeyMapper)
if input ~= nil then
assertIsType(input, "string", "input")
end
if listKeyMapper ~= nil then
assertIsType(listKeyMapper, "function", "listKeyMapper")
end
local useListKeys = type(listKeyMapper) == "function"
local parsed = {}
if input and #input > 0 then
local items = StringSplit(input, "[&;]")
for _, item in ipairs(items) do
if item and #item > 0 then
local key, value = unpack(string.split(item, "="))
if value == nil then
value = ""
end
key = urlDecode(key)
value = urlDecode(value)
if useListKeys then
if parsed[key] ~= nil then
local listKey = listKeyMapper(key)
if listKey == nil then
listKey = key
end
if type(parsed[listKey]) ~= "table" then
parsed[listKey] = { parsed[key] }
end
table.insert(parsed[listKey], value)
if key ~= listKey then
parsed[key] = value
end
else
parsed[key] = value
end
else
if parsed[key] == nil then
parsed[key] = {}
end
table.insert(parsed[key], value)
end
end
end
end
return parsed
end
return ParseQuery
@@ -0,0 +1,74 @@
return function()
local ParseQuery = require(script.Parent.ParseQuery)
describe("ParseQuery normal usage", function()
it("should parse a normal query string", function()
local parsed = ParseQuery("name1=value1&name2=value2")
expect(parsed.name1[1]).to.equal("value1")
expect(parsed.name2[1]).to.equal("value2")
end)
it("should parse multiple values for the same name", function()
local parsed = ParseQuery("name1=value1&name1=value2")
expect(#parsed.name1).to.equal(2)
expect(parsed.name1[1]).to.equal("value1")
expect(parsed.name1[2]).to.equal("value2")
end)
it("should allow custom keys for multiple values", function()
local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function(key)
return "list_of_" .. key
end)
expect(parsed.name1).to.equal("value1")
expect(parsed.name2).to.equal("value3")
expect(#parsed.list_of_name2).to.equal(2)
expect(parsed.list_of_name2[1]).to.equal("value2")
expect(parsed.list_of_name2[2]).to.equal("value3")
end)
it("should url decode names and values", function()
local parsed = ParseQuery("name1=value+1&%5Fname2=value2")
expect(parsed.name1[1]).to.equal("value 1")
expect(parsed._name2[1]).to.equal("value2")
end)
end)
describe("ParseQuery edge cases", function()
it("should return an empty table on empty or nil input", function()
local parsed1 = ParseQuery("")
local parsed2 = ParseQuery(nil)
expect(#parsed1).to.equal(0)
expect(#parsed2).to.equal(0)
end)
it("should support listKeyMapper returning key or nil", function()
local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function()end)
expect(parsed.name1).to.equal("value1")
expect(#parsed.name2).to.equal(2)
expect(parsed.name2[1]).to.equal("value2")
expect(parsed.name2[2]).to.equal("value3")
end)
it("should use empty strings for unavailable values", function()
local parsed = ParseQuery("name1=&=value2&name3", function()end)
expect(parsed.name1).to.equal("")
expect(parsed[""]).to.equal("value2")
expect(parsed.name3).to.equal("")
end)
it("should support `;` as separator", function()
local parsed = ParseQuery("name1=value1;name2=value2", function()end)
expect(parsed.name1).to.equal("value1")
expect(parsed.name2).to.equal("value2")
end)
end)
end
@@ -0,0 +1,26 @@
-- Performs multiple replacements on the given string.
-- replacements is a table where the keys are patterns to be replaced, and the
-- values are the replacements to be made. For example:
-- StringReplaceAll(
-- "key1 key2 key3",
-- {["%w+1"] = "value1", ["%w+2"] = "value2"}
-- )
-- becomes "value1 value2 key3".
return function(str, replacements)
if type(str) ~= "string" then
return ""
end
if type(replacements) ~= "table" then
return str
end
local result = str
for piiStr, replaceStr in pairs(replacements) do
if type(piiStr) == "string" and type(replaceStr) == "string" then
result = string.gsub(result, piiStr, replaceStr)
end
end
return result
end
@@ -0,0 +1,78 @@
return function()
local StringReplaceAll = require(script.Parent.StringReplaceAll)
it("should return empty string if str is not a string", function()
local result = StringReplaceAll(1, { "a" })
expect(result).to.equal("")
result = StringReplaceAll({ 1, 2, 3 }, { "a" })
expect(result).to.equal("")
result = StringReplaceAll(nil, { "a" })
expect(result).to.equal("")
end)
it("should return original string if replacements is not a table", function()
local result = StringReplaceAll("abc", "a")
expect(result).to.equal("abc")
result = StringReplaceAll("abc", 1)
expect(result).to.equal("abc")
result = StringReplaceAll("abc", nil)
expect(result).to.equal("abc")
end)
it("should replace a PII string if provided", function()
local result = StringReplaceAll("abc", { a = "" })
expect(result).to.equal("bc")
result = StringReplaceAll("abc", { b = "" })
expect(result).to.equal("ac")
result = StringReplaceAll("abc-123-a2c", { ["2"] = "" })
expect(result).to.equal("abc-13-ac")
result = StringReplaceAll("abc-123-a2c", { a = "A" })
expect(result).to.equal("Abc-123-A2c")
-- special character
result = StringReplaceAll("hello, said Noob_123", { ["Noob_123"] = "Username" })
expect(result).to.equal("hello, said Username")
end)
it("should replace all PII strings provided", function()
local result = StringReplaceAll("abc", { a = "A", c = "d"})
expect(result).to.equal("Abd")
result = StringReplaceAll("abc", { a = "A", bc = "" })
expect(result).to.equal("A")
result = StringReplaceAll("abc-123-a23", { abc = "user", ["123"] = "id" })
expect(result).to.equal("user-id-a23")
end)
it("should return original string if replacements is an empty table or no matches are found", function()
local result = StringReplaceAll("abc", {})
expect(result).to.equal("abc")
result = StringReplaceAll("abc", { d = "e" })
expect(result).to.equal("abc")
result = StringReplaceAll("abc", { d = "e", e = "f" })
expect(result).to.equal("abc")
end)
it("should ignore PIIs if they are not strings", function()
local result = StringReplaceAll("abc", { a = 1 })
expect(result).to.equal("abc")
result = StringReplaceAll("abc", { a = 1, bc = "" })
expect(result).to.equal("a")
end)
it("should match the example given in the doc string", function()
local result = StringReplaceAll("key1 key2 key3", {["%w+1"] = "value1", ["%w+2"] = "value2"})
expect(result).to.equal("value1 value2 key3")
end)
end
@@ -0,0 +1,45 @@
-- Split the input string according to the separator pattern into at most limit
-- pieces. If the separator is nil, it defaults to consecutive whitespace (%s+).
-- If the limit isn't defined, it will split the string into as many pieces as
-- it finds.
-- Note: There is now a string.split function built into Luau though it isn't an
-- exact replacement for this.
local function StringSplit(input, separator, limit)
if #input == 0 then
if string.find(input, separator) then
return {}
else
return {""}
end
end
if not limit then
limit = -1
end
if limit == 1 then
return {input}
end
if not separator then
separator = "%s+"
end
local start, stop = string.find(input, separator)
if not start then
return {input}
end
-- special case, delimiter resolved to ""
if stop < 1 then
start, stop = string.find(string.sub(input, 2), separator)
start = start + 1
stop = stop + 1
end
local first = string.sub(input, 1, start - 1)
local rest = string.sub(input, stop + 1)
-- special case, non empty pattern found at the end
if #rest == 0 and stop >= start then
return {first, rest}
end
local items = StringSplit(rest, separator, limit - 1)
table.insert(items, 1, first)
return items
end
return StringSplit
@@ -0,0 +1,108 @@
return function()
local StringSplit = require(script.Parent.StringSplit)
describe("Normal usage", function()
it("should split a string by a one character separator", function()
local str = "Roblox Powering Imagination"
local words = StringSplit(str, " ")
expect(#words).to.equal(3)
expect(words[1]).to.equal("Roblox")
expect(words[2]).to.equal("Powering")
expect(words[3]).to.equal("Imagination")
end)
it("should split a string by a complex regex", function()
local str = "https://corp.roblox.com/technology"
local words = StringSplit(str, "[^a-z]+")
expect(#words).to.equal(5)
expect(words[1]).to.equal("https")
expect(words[2]).to.equal("corp")
expect(words[3]).to.equal("roblox")
expect(words[4]).to.equal("com")
expect(words[5]).to.equal("technology")
end)
it("should split on blank spaces by default", function()
local str = "together through\n\tplay"
local words = StringSplit(str)
expect(#words).to.equal(3)
expect(words[1]).to.equal("together")
expect(words[2]).to.equal("through")
expect(words[3]).to.equal("play")
end)
it("should not exceed the provided limit", function()
local str = "Modules/Common/StringUtilities/StringSplit"
local words = StringSplit(str, "/", 3)
expect(#words).to.equal(3)
expect(words[1]).to.equal("Modules")
expect(words[2]).to.equal("Common")
expect(words[3]).to.equal("StringUtilities/StringSplit")
end)
it("should give en empty string on leading, repeated and trailing separators", function()
local str = "/var//www/"
local words = StringSplit(str, "/")
expect(#words).to.equal(5)
expect(words[1]).to.equal("")
expect(words[2]).to.equal("var")
expect(words[3]).to.equal("")
expect(words[4]).to.equal("www")
expect(words[5]).to.equal("")
end)
end)
describe("Edge case:", function()
it("an empty string results in one empty result", function()
local str = ""
local words = StringSplit(str, "/")
expect(#words).to.equal(1)
expect(words[1]).to.equal("")
end)
it("an empty separator splits to characters, no leading/trailing empty strings", function()
local str = "lua"
local words = StringSplit(str, "")
expect(#words).to.equal(3)
expect(words[1]).to.equal("l")
expect(words[2]).to.equal("u")
expect(words[3]).to.equal("a")
end)
it("empty string AND separator should get an empty array", function()
local str = ""
local words = StringSplit(str, "")
expect(#words).to.equal(0)
end)
it("regex separators can resolve to empty and non empty in same operation", function()
local str = "//r#blox"
local words = StringSplit(str, "[^a-z]*")
expect(#words).to.equal(6)
-- pattern resolved to "//", creating a leading empty string
expect(words[1]).to.equal("")
-- pattern resolved to "#"
expect(words[2]).to.equal("r")
-- pattern resolved to "", splitting by character
expect(words[3]).to.equal("b")
expect(words[4]).to.equal("l")
expect(words[5]).to.equal("o")
expect(words[6]).to.equal("x")
-- pattern resolved to "", NO trailing empty string
end)
end)
end
@@ -0,0 +1,34 @@
-- Trim the given set of characters from both ends of the input string. `sides`
-- can be set to {left = true} or {right = true} to trim from just one side.
local function StringTrim(input, chars, sides)
if not chars then
chars = "%s"
end
if #chars == 0 then
return input
end
if not sides then
sides = {
left = true,
right = true,
}
end
local trimmed = input
if sides.left then
local start = string.find(trimmed, "[^" .. chars .. "]")
if not start then
return ""
end
trimmed = string.sub(trimmed, start)
end
if sides.right then
local stop = string.find(trimmed, "[" .. chars .. "]+$")
if not stop then
return trimmed
end
trimmed = string.sub(trimmed, 1, stop-1)
end
return trimmed
end
return StringTrim
@@ -0,0 +1,54 @@
return function()
local StringTrim = require(script.Parent.StringTrim)
describe("Normal usage", function()
it("should trim the specified character", function()
local str = "/Modules/Common/StringUtilities/"
local trimmed = StringTrim(str, "/")
expect(trimmed).to.equal("Modules/Common/StringUtilities")
end)
it("should accept multiple charcters", function()
local str = "(Roblox Powering Imagination){}"
local trimmed = StringTrim(str, "(){}")
expect(trimmed).to.equal("Roblox Powering Imagination")
end)
it("should be able to trim only on the right side", function()
local str = "(Roblox Powering Imagination){}"
local trimmed = StringTrim(str, "(){}", {right = true})
expect(trimmed).to.equal("(Roblox Powering Imagination")
end)
it("should be able to trim only on the left side", function()
local str = "(Roblox Powering Imagination){}"
local trimmed = StringTrim(str, "(){}", {left = true})
expect(trimmed).to.equal("Roblox Powering Imagination){}")
end)
it("should default to trimming blanks on both sides", function()
local str = "\tRoblox Powering Imagination \n"
local trimmed = StringTrim(str)
expect(trimmed).to.equal("Roblox Powering Imagination")
end)
end)
describe("Edge case:", function()
it("an empty character list is a no-op", function()
local str = " Roblox Powering Imagination "
local trimmed = StringTrim(str, "")
expect(trimmed).to.equal(str)
end)
end)
end
@@ -0,0 +1,6 @@
return {
ParseQuery = require(script.ParseQuery),
StringReplaceAll = require(script.StringReplaceAll),
StringSplit = require(script.StringSplit),
StringTrim = require(script.StringTrim),
}
@@ -0,0 +1,5 @@
return function()
it("should import without error", function()
require(script.Parent)
end)
end