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,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,68 @@
--[[
A helper function to define a Rodux action creator with an associated name.
Normally when creating a Rodux action, you can just create a function:
return function(value)
return {
type = "MyAction",
value = value,
}
end
And then when you check for it in your reducer, you either use a constant,
or type out the string name:
if action.type == "MyAction" then
-- change some state
end
Typos here are a remarkably common bug. We also have the issue that there's
no link between reducers and the actions that they respond to!
`Action` (this helper) provides a utility that makes this a bit cleaner.
Instead, define your Rodux action like this:
return Action("MyAction", function(value)
return {
value = value,
}
end)
We no longer need to add the `type` field manually.
Additionally, the returned action creator now has a 'name' property that can
be checked by your reducer:
local MyAction = require(Reducers.MyAction)
...
if action.type == MyAction.name then
-- change some state!
end
Now we have a clear link between our reducers and the actions they use, and
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
at runtime!
]]
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
@@ -0,0 +1,85 @@
return function()
local Action = require(script.Parent.Action)
it("should return a table", function()
local action = Action("foo", function()
return {}
end)
expect(action).to.be.a("table")
end)
it("should set the name of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action.name).to.equal("foo")
end)
it("should be able to be called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action).never.to.throw()
end)
it("should return a table when called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action()).to.be.a("table")
end)
it("should set the type of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action().type).to.equal("foo")
end)
it("should set values", function()
local action = Action("foo", function(value)
return {
value = value
}
end)
expect(action(100).value).to.equal(100)
end)
it("should throw when passed a function", function()
local action = Action("foo", function()
return function() end
end)
expect(action).to.throw()
end)
it("should throw with a invalid name", function()
expect(function()
Action(nil, function()
return {}
end)
end).to.throw()
expect(function()
Action(100, function()
return {}
end)
end).to.throw()
end)
it("should throw when passed a invalid function", function()
expect(function()
Action("foo", nil)
end).to.throw()
expect(function()
Action("foo", {})
end).to.throw()
end)
end
@@ -0,0 +1,19 @@
local Color = {}
function Color.RgbFromHex(hexColor)
assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range")
local b = hexColor % 256
hexColor = (hexColor - b) / 256
local g = hexColor % 256
hexColor = (hexColor - g) / 256
local r = hexColor
return r, g, b
end
function Color.Color3FromHex(hexColor)
return Color3.fromRGB(Color.RgbFromHex(hexColor))
end
return Color
@@ -0,0 +1,33 @@
return function()
local Color = require(script.Parent.Color)
describe("RgbFromHex", function()
it("should convert a hex color to rgb correctly", function()
local r, g, b = Color.RgbFromHex(0x232527)
expect(r).to.equal(35)
expect(g).to.equal(37)
expect(b).to.equal(39)
r, g, b = Color.RgbFromHex(0x0)
expect(r).to.equal(0)
expect(g).to.equal(0)
expect(b).to.equal(0)
r, g, b = Color.RgbFromHex(0xffffff)
expect(r).to.equal(255)
expect(g).to.equal(255)
expect(b).to.equal(255)
end)
it("should assert if given a hex color out of range", function()
expect(function()
Color.RgbFromHex(-1)
end).to.throw()
expect(function()
Color.RgbFromHex(0x1000000)
end).to.throw()
end)
end)
end
@@ -0,0 +1,131 @@
--[[
Provides an implementation of functional programming primitives.
]]
local Functional = {}
--[[
Create a copy of a list with only values for which `callback` returns true
]]
function Functional.Filter(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
if callback(value, key) then
table.insert(new, value)
end
end
return new
end
--[[
Create a copy of a list where each value is transformed by `callback`
]]
function Functional.Map(list, callback)
local new = {}
for key = 1, #list do
new[key] = callback(list[key], key)
end
return new
end
--[[
Identical to Map, except that the result will be reversed.
]]
function Functional.MapReverse(list, callback)
local new = {}
for key = #list, 1, -1 do
new[key] = callback(list[key], key)
end
return new
end
--[[
Create a copy of a list doing a combination filter and map.
If callback returns nil for any item, it is considered filtered from the
list. Any other value is considered the result of the 'map' operation.
]]
function Functional.FilterMap(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
local result = callback(value, key)
if result ~= nil then
table.insert(new, result)
end
end
return new
end
--[[
Performs a left-fold of the list with the given initial value and callback.
]]
function Functional.Fold(list, initial, callback)
local accum = initial
for key = 1, #list do
accum = callback(accum, list[key], key)
end
return accum
end
--[[
Performs a fold over the entries in the given dictionary.
]]
function Functional.FoldDictionary(dictionary, initial, callback)
local accum = initial
for key, value in pairs(dictionary) do
accum = callback(accum, key, value)
end
return accum
end
--[[
Returns a list that contains at most `count` values from the given list.
]]
function Functional.Take(list, count, startingIndex)
startingIndex = startingIndex or 1
local maxIndex = count + (startingIndex - 1)
if maxIndex > #list then
maxIndex = #list
end
local new = {}
for i = startingIndex, maxIndex do
local value = list[i]
local newIndex = i - (startingIndex - 1)
new[newIndex] = value
end
return new
end
--[[
If the list contains the sought-after element, return its index, or nil otherwise.
]]
function Functional.Find(list, value)
for index, element in ipairs(list) do
if element == value then
return index
end
end
return nil
end
return Functional
@@ -0,0 +1,218 @@
return function()
local Functional = require(script.Parent.Functional)
local function identity(...)
return ...
end
local function add(a, b)
return a + b
end
describe("Filter", function()
it("should copy lists correctly", function()
local listA = {1, 2, 3}
local listB = Functional.Filter(listA, function()
return true
end)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter predicate", function()
local listA = {1, 2, 3, 4, 5}
local listB = Functional.Filter(listA, function(value, key)
expect(value).to.equal(key)
return value % 2 == 0
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(4)
end)
end)
describe("Map", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
end)
describe("MapReverse", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
it("should iterate backwards", function()
local list = {1, 2, 3}
local nextKey = 3
Functional.MapReverse(list, function(value, key)
expect(value).to.equal(nextKey)
expect(key).to.equal(nextKey)
nextKey = nextKey - 1
end)
expect(nextKey).to.equal(0)
end)
end)
describe("FilterMap", function()
it("should copy truthy lists using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.FilterMap(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter-map predicate", function()
local listA = {1, 2, 3, 4, 5}
-- Create a list containing only the odd numbers, and double those numbers
local listB = Functional.FilterMap(listA, function(value, key)
expect(value).to.equal(key)
if value % 2 == 0 then
return nil
end
return value * 2
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(6)
expect(listB[3]).to.equal(10)
end)
end)
describe("Fold", function()
it("should left-fold lists", function()
local list = {1, 2, 3, 4, 5}
local sum = Functional.Fold(list, 0, add)
expect(sum).to.equal(15)
end)
end)
describe("Take", function()
it("should take values from a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
end)
it("should not take past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(3)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
end)
it("should copy all values when taking past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(#a)
expect(a[1]).to.equal(b[1])
expect(a[2]).to.equal(b[2])
expect(a[3]).to.equal(b[3])
end)
it("should take values from a starting index when provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 2, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(2)
expect(b[2]).to.equal(3)
end)
it("should not take past the end of a list when the starting index is provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 3, 3)
expect(#b).to.equal(2)
expect(b[1]).to.equal(3)
expect(b[2]).to.equal(4)
end)
end)
describe("Find", function()
it("should return index of matched item", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should find the first example in the case of duplicates", function()
local a = {"foo", "bar", "garply", "bar"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should return nil if item is not found", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "fleebledegoop")
expect(b).to.equal(nil)
end)
end)
end
@@ -0,0 +1,141 @@
--[[
Provides functions for manipulating immutable data structures.
]]
local Immutable = {}
--[[
Merges dictionary-like tables together.
]]
function Immutable.JoinDictionaries(...)
local result = {}
for i = 1, select("#", ...) do
local dictionary = select(i, ...)
for key, value in pairs(dictionary) do
result[key] = value
end
end
return result
end
--[[
Joins any number of lists together into a new list
]]
function Immutable.JoinLists(...)
local new = {}
for listKey = 1, select("#", ...) do
local list = select(listKey, ...)
local len = #new
for itemKey = 1, #list do
new[len + itemKey] = list[itemKey]
end
end
return new
end
--[[
Creates a new copy of the dictionary and sets a value inside it.
]]
function Immutable.Set(dictionary, key, value)
local new = {}
for key, value in pairs(dictionary) do
new[key] = value
end
new[key] = value
return new
end
--[[
Creates a new copy of the list with the given elements appended to it.
]]
function Immutable.Append(list, ...)
local new = {}
local len = #list
for key = 1, len do
new[key] = list[key]
end
for i = 1, select("#", ...) do
new[len + i] = select(i, ...)
end
return new
end
--[[
Remove elements from a dictionary
]]
function Immutable.RemoveFromDictionary(dictionary, ...)
local result = {}
for key, value in pairs(dictionary) do
local found = false
for listKey = 1, select("#", ...) do
if key == select(listKey, ...) then
found = true
break
end
end
if not found then
result[key] = value
end
end
return result
end
--[[
Remove the given key from the list.
]]
function Immutable.RemoveFromList(list, removeIndex)
local new = {}
for i = 1, #list do
if i ~= removeIndex then
table.insert(new, list[i])
end
end
return new
end
--[[
Remove the range from the list starting from the index.
]]
function Immutable.RemoveRangeFromList(list, index, count)
local new = {}
for i = 1, #list do
if i < index or i >= index + count then
table.insert(new, list[i])
end
end
return new
end
--[[
Creates a new list that has no occurrences of the given value.
]]
function Immutable.RemoveValueFromList(list, removeValue)
local new = {}
for i = 1, #list do
if list[i] ~= removeValue then
table.insert(new, list[i])
end
end
return new
end
return Immutable
@@ -0,0 +1,284 @@
return function()
local Immutable = require(script.Parent.Immutable)
describe("JoinDictionaries", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinDictionaries(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values like dictionary values", function()
local a = {
[1] = 1,
[2] = 2,
[3] = 3
}
local b = {
[1] = 11,
[2] = 22
}
local c = Immutable.JoinDictionaries(a, b)
expect(c[1]).to.equal(b[1])
expect(c[2]).to.equal(b[2])
expect(c[3]).to.equal(a[3])
end)
it("should merge dictionary values correctly", function()
local a = {
hello = "world",
foo = "bar"
}
local b = {
foo = "baz",
tux = "penguin"
}
local c = Immutable.JoinDictionaries(a, b)
expect(c.hello).to.equal(a.hello)
expect(c.foo).to.equal(b.foo)
expect(c.tux).to.equal(b.tux)
end)
it("should merge multiple dictionaries", function()
local a = {
foo = "yes"
}
local b = {
bar = "yup"
}
local c = {
baz = "sure"
}
local d = Immutable.JoinDictionaries(a, b, c)
expect(d.foo).to.equal(a.foo)
expect(d.bar).to.equal(b.bar)
expect(d.baz).to.equal(c.baz)
end)
end)
describe("JoinLists", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinLists(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values correctly", function()
local a = {1, 2, 3}
local b = {4, 5, 6}
local c = Immutable.JoinLists(a, b)
expect(#c).to.equal(6)
for i = 1, #c do
expect(c[i]).to.equal(i)
end
end)
it("should merge multiple lists", function()
local a = {1, 2}
local b = {3, 4}
local c = {5, 6}
local d = Immutable.JoinLists(a, b, c)
expect(#d).to.equal(6)
for i = 1, #d do
expect(d[i]).to.equal(i)
end
end)
end)
describe("Set", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Set(a, "foo", "bar")
expect(b).never.to.equal(a)
end)
it("should treat numeric keys normally", function()
local a = {1, 2, 3}
local b = Immutable.Set(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(4)
expect(b[3]).to.equal(3)
end)
it("should overwrite dictionary-like keys", function()
local a = {
foo = "bar",
baz = "qux"
}
local b = Immutable.Set(a, "foo", "hello there")
expect(b.foo).to.equal("hello there")
expect(b.baz).to.equal(a.baz)
end)
end)
describe("Append", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Append(a, "another happy landing")
expect(b).never.to.equal(a)
end)
it("should append values", function()
local a = {1, 2, 3}
local b = Immutable.Append(a, 4, 5)
expect(#b).to.equal(5)
for i = 1, #b do
expect(b[i]).to.equal(i)
end
end)
end)
describe("RemoveFromDictionary", function()
it("should preserve immutability", function()
local a = { foo = "bar" }
local b = Immutable.RemoveFromDictionary(a, "foo")
expect(b).to.never.equal(a)
end)
it("should remove fields from the dictionary", function()
local a = {
foo = "bar",
baz = "qux",
boof = "garply",
}
local b = Immutable.RemoveFromDictionary(a, "foo", "boof")
expect(b.foo).to.never.be.ok()
expect(b.baz).to.equal("qux")
expect(b.boof).to.never.be.ok()
end)
end)
describe("RemoveFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b).never.to.equal(a)
end)
it("should remove elements from the list", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
describe("RemoveRangeFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b).never.to.equal(a)
end)
it("should remove elements properly from the list 1", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 2", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 1, 4)
expect(b[1]).to.equal(5)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 3", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 4", function()
local a = {1, 2, 3, 4, 5, 6, 7}
local b = Immutable.RemoveRangeFromList(a, 4, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
expect(b[4]).never.to.be.ok()
end)
it("should not remove any elements when count is 0 or less", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 0)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
local c = Immutable.RemoveRangeFromList(a, 2, -1)
expect(c[1]).to.equal(1)
expect(c[2]).to.equal(2)
expect(c[3]).to.equal(3)
end)
end)
describe("RemoveValueFromList", function()
it("should preserve immutability", function()
local a = {1, 1, 1}
local b = Immutable.RemoveValueFromList(a, 1)
expect(b).never.to.equal(a)
end)
it("should remove all elements from the list", function()
local a = {1, 2, 2, 3}
local b = Immutable.RemoveValueFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
end
@@ -0,0 +1,75 @@
--[[
A limited, simple implementation of a Signal.
Handlers are fired in order, and (dis)connections are properly handled when
executing an event.
Signal uses Immutable to avoid invalidating the 'Fire' loop iteration.
]]
local Immutable = require(script.Parent.Immutable)
local Signal = {}
Signal.__index = Signal
function Signal.new()
local self = {
_listeners = {}
}
setmetatable(self, Signal)
return self
end
function Signal:connect(callback)
local listener = {
callback = callback,
isConnected = true,
}
self._listeners = Immutable.Append(self._listeners, listener)
local function disconnect()
listener.isConnected = false
self._listeners = Immutable.RemoveValueFromList(self._listeners, listener)
end
return {
Disconnect = function()
warn(string.format(
"Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]",
debug.traceback()
))
disconnect()
end,
disconnect = disconnect,
}
end
function Signal:fire(...)
for _, listener in ipairs(self._listeners) do
if listener.isConnected then
listener.callback(...)
end
end
end
function Signal:Connect(...)
warn(string.format(
"Signal:Connect() has been deprecated, use Signal:connect()\n%s]",
debug.traceback()
))
return self:connect(...)
end
function Signal:Fire(...)
warn(string.format(
"Signal:Fire() has been deprecated, use Signal:fire()\n%s]",
debug.traceback()
))
self:fire(...)
end
return Signal
@@ -0,0 +1,114 @@
return function()
local Signal = require(script.Parent.Signal)
it("should construct from nothing", function()
local signal = Signal.new()
expect(signal).to.be.ok()
end)
it("should fire connected callbacks", function()
local callCount = 0
local value1 = "Hello World"
local value2 = 7
local callback = function(arg1, arg2)
expect(arg1).to.equal(value1)
expect(arg2).to.equal(value2)
callCount = callCount + 1
end
local signal = Signal.new()
local connection = signal:connect(callback)
signal:fire(value1, value2)
expect(callCount).to.equal(1)
connection:disconnect()
signal:fire(value1, value2)
expect(callCount).to.equal(1)
end)
it("should disconnect handlers", function()
local callback = function()
error("Callback was called after disconnect!")
end
local signal = Signal.new()
local connection = signal:connect(callback)
connection:disconnect()
signal:fire()
end)
it("should fire handlers in order", function()
local signal = Signal.new()
local x = 0
local y = 0
local callback1 = function()
expect(x).to.equal(0)
expect(y).to.equal(0)
x = x + 1
end
local callback2 = function()
expect(x).to.equal(1)
expect(y).to.equal(0)
y = y + 1
end
signal:connect(callback1)
signal:connect(callback2)
signal:fire()
expect(x).to.equal(1)
expect(y).to.equal(1)
end)
it("should continue firing despite mid-event disconnection", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionA
connectionA = signal:connect(function()
connectionA:disconnect()
countA = countA + 1
end)
signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(1)
end)
it("should skip listeners that were disconnected during event evaluation", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionB
signal:connect(function()
countA = countA + 1
connectionB:disconnect()
end)
connectionB = signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(0)
end)
end
@@ -0,0 +1,113 @@
local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp")
local TextService = game:GetService("TextService")
local Text = {}
-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results
local MAX_BOUND = 10000
-- Remove with EngineFeatureTextBoundsRoundUp
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
if not EngineFeatureTextBoundsRoundUp then
Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2)
end
-- Wrapper function for GetTextSize
function Text.GetTextBounds(text, font, fontSize, bounds)
return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING
end
function Text.GetTextWidth(text, font, fontSize)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X
end
function Text.GetTextHeight(text, font, fontSize, widthCap)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y
end
-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation
function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker)
overflowMarker = overflowMarker or ""
if Text.GetTextWidth(text, font, fontSize) > widthInPixels then
-- A binary search may be more efficient
local lastText = ""
for _, stopIndex in utf8.graphemes(text) do
local newText = string.sub(text, 1, stopIndex) .. overflowMarker
if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then
return lastText
end
lastText = newText
end
else -- No truncation needed
return text
end
return ""
end
function Text.TruncateTextLabel(textLabel, overflowMarker)
textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font,
textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker)
end
-- Remove whitespace from the beginning and end of the string
function Text.Trim(str)
if type(str) ~= "string" then
error(string.format("Text.Trim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s*(.-)%s*$", "%1"))
end
-- Remove whitespace from the end of the string
function Text.RightTrim(str)
if type(str) ~= "string" then
error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+$", ""))
end
-- Remove whitespace from the beginning of the string
function Text.LeftTrim(str)
if type(str) ~= "string" then
error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s+", ""))
end
-- Replace multiple whitespace with one; remove leading and trailing whitespace
function Text.SpaceNormalize(str)
if type(str) ~= "string" then
error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , ""))
end
-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text.
function Text.Split(str, pattern)
if type(str) ~= "string" then
error(string.format("Text.Split called on non-string type %s.", type(str)), 2)
elseif type(pattern) ~= "string" then
error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2)
elseif pattern == "" then
error("Text.Split called with an empty pattern.", 2)
end
local result = {}
local currentPosition = 1
while true do
local patternStart, patternEnd = string.find(str, pattern, currentPosition, true)
if not patternStart or not patternEnd then break end
table.insert(result, string.sub(str, currentPosition, patternStart - 1))
currentPosition = patternEnd + 1
end
table.insert(result, string.sub(str, currentPosition, string.len(str)))
return result
end
return Text
@@ -0,0 +1,410 @@
return function()
local Text = require(script.Parent.Text)
describe("GetTextBounds", function()
it("should return a bounds of padding width and font-size height when the string is empty", function()
local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x)
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height and width of a string as one line with large bounds", function()
local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height of the string as multiple lines with short bounds", function()
local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000))
expect(bounds.Y > 18).to.equal(true)
end)
end)
describe("GetTextHeight", function()
it("should return height equal to font size when string is empty", function()
local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0)
expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
end)
describe("GetTextWidth", function()
it("should return width equal to 1 when string is empty", function()
local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18)
expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x)
end)
end)
describe("Truncate", function()
it("should return empty string", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should return empty string for not empty box", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should truncate with ...", function()
local reallyLongQuery = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...")
expect(reallyLongQuery).to.equal("One Two Thre...")
end)
it("should truncate without a ...", function()
local reallyLongQueryNoOverflowMarker = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100)
expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ")
end)
it("should not truncate", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100)
expect(shouldFitQuery).to.equal("One Two")
end)
it("should not truncate, off by one check", function()
local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100)
expect(oneCharQuery).to.equal("O")
end)
it("should truncate, off by one check", function()
local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0)
expect(oneCharNoRoomQuery).to.equal("")
end)
it("should perform a negative width check", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...")
expect(shouldFitQuery).to.equal("")
end)
itFIXME("should truncate long graphemes properly", function()
-- 11-byte rainbow flag grapheme
-- Flag, zero-space-joiner, rainbow
local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752)
local oneFlagWithinLimit = Text.Truncate(
rainbowFlag, Enum.Font.SourceSans, 18, 100, "...")
expect(oneFlagWithinLimit).to.equal(rainbowFlag)
local twoRainbowFlags = rainbowFlag .. rainbowFlag
local twoFlagsAreFine = Text.Truncate(
twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(twoFlagsAreFine).to.equal(twoRainbowFlags)
local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags
local fourFlagsIsTooLong = Text.Truncate(
fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation
end)
end)
describe("TruncateTextLabel", function()
it("should use text label attributes to truncate text", function()
local screenGui = Instance.new("ScreenGui")
local textLabel = Instance.new("TextLabel")
textLabel.Size = UDim2.new(0, 100, 0, 32)
textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
textLabel.Font = Enum.Font.SourceSans
textLabel.TextSize = 18
textLabel.Parent = screenGui
Text.TruncateTextLabel(textLabel)
expect(textLabel.Text).to.equal("One Two Three ")
end)
end)
describe("TrimString", function()
it("Should trim the string properly 1", function()
local trimmedInput = Text.Trim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 2", function()
local trimmedInput = Text.Trim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 3", function()
local trimmedInput = Text.Trim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 4", function()
local trimmedInput = Text.Trim(" ab ")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 5", function()
local trimmedInput = Text.Trim(" a b ")
local expected = "a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 6", function()
local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ")
local expected = "a"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string with unicode characters properly", function()
local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 7", function()
local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 8", function()
local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("RightTrimString", function()
it("Should right trim the string properly 1", function()
local trimmedInput = Text.RightTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 2", function()
local trimmedInput = Text.RightTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 3", function()
local trimmedInput = Text.RightTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 4", function()
local trimmedInput = Text.RightTrim(" ab ")
local expected = " ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 5", function()
local trimmedInput = Text.RightTrim(" a b ")
local expected = " a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 6", function()
local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ")
local expected = "\r\n\t\f a"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string with unicode characters properly", function()
local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 7", function()
local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 8", function()
local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("LeftTrimString", function()
it("Should left trim the string properly 1", function()
local trimmedInput = Text.LeftTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 2", function()
local trimmedInput = Text.LeftTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 3", function()
local trimmedInput = Text.LeftTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 4", function()
local trimmedInput = Text.LeftTrim(" ab ")
local expected = "ab "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 5", function()
local trimmedInput = Text.LeftTrim(" a b ")
local expected = "a b "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 6", function()
local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ")
local expected = "a\r\n\t\f "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string with unicode characters properly", function()
local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 7", function()
local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly", function()
local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("SpaceNormalize", function()
it("should remove multiple spaces between words", function()
local a = "This is not a normal sentence."
expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.")
end)
it("should remove leading and trailing whitespace", function()
local a = " SpaceTabSpaceTab "
expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab")
end)
it("should not change a string with no whitespace", function()
local a = "There'sNo%Whit.e\\space--InThis."
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should remove all whitespace in a string that is nothing but whitespace", function()
local a = " "
expect(Text.SpaceNormalize(a)).to.equal("")
end)
it("should handle the case where the string is empty", function()
local a = ""
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
expect(function()
Text.SpaceNormalize(a)
end).to.throw()
end)
end)
describe("Split", function()
local function tableEquals(tb1, tb2)
local tables = { tb1, tb2 }
for _,tb in ipairs(tables) do
for key in pairs(tb) do
if tb1[key] ~= tb2[key] then
return false
end
end
end
return true
end
it("should return the correct table for your standard use case", function()
local a = "this,is,comma,separated"
local pattern = ","
local expectedResult = {
[1] = "this",
[2] = "is",
[3] = "comma",
[4] = "separated",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should not remove whitespace", function()
local a = " SpaceTab , , Space"
local pattern = ","
local expectedResult = {
[1] = " SpaceTab ",
[2] = " ",
[3] = " Space",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should treat regular expressions as plain text", function()
local a = "Notyour^%s+normalstring.Thisisasecondsentence."
local b = "."
local c = "^%s+"
local d = "%A"
local expectedB = {
[1] = "Notyour^%s+normalstring",
[2] = "Thisisasecondsentence",
[3] = "",
}
local expectedC = {
[1] = "Notyour",
[2] = "normalstring.Thisisasecondsentence."
}
local expectedD = {
[1] = "Notyour^%s+normalstring.Thisisasecondsentence."
}
expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true)
expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true)
expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true)
end)
it("should work when pattern is not in string", function()
local a = "The pattern you are looking for does not exist."
local pattern = ","
local expectedResult = {
[1] = "The pattern you are looking for does not exist.",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should work when called on an empty string", function()
local a = ""
local pattern = ","
local expectedResult = {
[1] = "",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should throw an error if called on an empty pattern", function()
local a = "The pattern definitely doesn't exist here."
local pattern = ""
expect(function()
Text.Split(a, pattern)
end).to.throw()
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
local b = "an actual string"
expect(function()
Text.Split(a, b)
end).to.throw()
expect(function()
Text.Split(b, a)
end).to.throw()
end)
end)
end
@@ -0,0 +1,68 @@
--[[␍
memoize creates a function as a wrapper that caches the last outputs of a function.␍
This is useful if you know that the function should return the same output every␍
time it is run with the same inputs. The function should only return an output, and␍
not have any side effects. These side effects are not cached.␍
Without memoize's caching, even though the function ouputs the same values, the␍
memory locations of the values are different; tables made in the function, even if␍
they have the same values, won't be the same tables.␍
memoize only caches the last set of inputs and ouputs. This means that it is only␍
helpful when the function is likely to be called with the same inputs multiple␍
times in a row. This is the case with most Roact use cases.␍
Note that memoize only does a ** shallow check on table inputs ** . This means␍
that if the same table is input but the elements of the table are different then␍
it will be assumed that the table has not changed.␍
In addition to all the previous warnings, memoize strips trailing nils. This means␍
that if foo is a memoized function and we call foo(), then foo(nil) will return a␍
cached value. This is opposed to how print handles input. print() only outputs a␍
new line, but print(nil) outputs "nil". This is because varargs can detect the␍
number of arguments passed in. So, be careful when using memoize with varargs.␍
Trailing nils will be stripped.␍
The wrapper can take any number of inputs and give any number of outputs.␍
Leading and interspersed nils are handled gracefully. Trailing nils on the input␍
are stripped.␍
]]
local function captureSize(...)
return {...}, select("#", ...)
end
local function memoize(func)
assert(type(func) == "function", "memoize requires a function to memoize")
local lastArgs
local lastNumArgs
local lastOutput
local lastNumOutput
return function(...)
local numArgs = select("#", ...)
while numArgs > 0 and select(numArgs, ...) == nil do
numArgs = numArgs - 1
end
if numArgs ~= lastNumArgs then
lastArgs = {...}
lastNumArgs = numArgs
lastOutput, lastNumOutput = captureSize(func(...))
return unpack(lastOutput, 1, lastNumOutput)
end
for i = 1, lastNumArgs do
if select(i, ...) ~= lastArgs[i] then
lastArgs = {...}
lastOutput, lastNumOutput = captureSize(func(...))
break
end
end
return unpack(lastOutput, 1, lastNumOutput)
end
end
return memoize
@@ -0,0 +1,213 @@
return function()
local memoize = require(script.Parent.memoize)
describe("memoize", function()
it("should handle arity 0", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
expect(identity()).to.equal(nil)
expect(identity(nil)).to.equal(nil)
expect(identity(nil, nil)).to.equal(nil)
expect(callCount).to.equal(1)
end)
it("should handle arity 1", function()
local callCount = 0
local identity = memoize(function(a)
callCount = callCount + 1
return a
end)
expect(identity(5)).to.equal(5)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(1)
expect(identity(6)).to.equal(6)
expect(callCount).to.equal(2)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(3)
end)
it("should handle arity 2", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(6, 5)
expect(a).to.equal(6)
expect(b).to.equal(5)
expect(callCount).to.equal(2)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(3)
end)
it("should handle mixed arity", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle trailing nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, nil)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle leading nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(3)
end)
it("should handle interspersed nils", function()
local callCount = 0
local identity = memoize(function(a, b, c, d)
callCount = callCount + 1
return a, b, c, d
end)
local a, b, c, d
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
-- Trailing nils can affect how interspersed nils are handled
a, b, c, d = identity(7, nil, 7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(1)
a, b, c, d = identity(7, nil, nil, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(nil)
expect(d).to.equal(nil)
expect(callCount).to.equal(2)
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(3)
end)
end)
end