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, 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)
@@ -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