First file translated to lua

This commit is contained in:
Matt Hargett 2023-04-03 16:15:27 -07:00
parent a370f468a4
commit f1533bfd9c
12 changed files with 742 additions and 261 deletions

48
.gitignore vendored
View file

@ -1,4 +1,46 @@
node_modules
yarn.lock
# always ignore files
*.DS_Store
.idea
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
# include common debug launch configs
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/launch.sample.json
*.sublime-*
.settings/
# test related, or directories generated by tests
test/actual
actual
coverage
.nyc_output
.nyc*
debug.log
/luacov.*
/lcov*
/site
/output.txt
cachegrind.out.*
callgrind.out.*
# npm
node_modules
npm-debug.log
# yarn
yarn.lock
yarn-error.log
# misc
_gh_pages
_draft
_drafts
bower_components
vendor
temp
tmp
TODO.md
package-lock.json

6
default.project.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "chalk",
"tree": {
"$path": "source"
}
}

6
foreman.toml Normal file
View file

@ -0,0 +1,6 @@
[tools]
luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "1.18.1" }
rojo = { source = "rojo-rbx/rojo", version = "7.2.1" }
selene = { source = "Kampfkarren/selene", version = "0.25.0" }
stylua = { source = "JohnnyMorganz/StyLua", version = "0.17.1" }
wally = { source = "UpliftGames/wally", version = "=0.3.1" }

7
selene.toml Normal file
View file

@ -0,0 +1,7 @@
std="roblox+testez"
[config]
empty_if = { comments_count = true }
[rules]
incorrect_standard_library_use = "allow"
shadowing = "allow"

186
source/array.lua Normal file
View file

@ -0,0 +1,186 @@
--[[
derived from documentation and reference implementation at:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
Attributions and copyright licensing by Mozilla Contributors is licensed under CC-BY-SA 2.5
These are heuristcs tested across 300+KLOC of JS translated to Lua
It isn't perfect, but it works for dozens of packages in the React, Jest, and GraphQL ecosystem
In those dozens of packages, hundreds and hundreds of tests were ported to Lua and passing
Additionally, the packages were integrated into a 500+KLOC 60fps product and shipped to 150+M global users
Reimplemented by Matt Hargett, 2023
]]
--!strict
local exports = {}
export type Array<T> = { [number]: T }
type Object = { [string]: any }
exports.indexOf = function<T>(haystack: Array<T>, needle: T, startIndex_: number?): number
local length = #haystack
local startIndex = if startIndex_ == nil
then 1
else if startIndex_ < 1 then math.max(1, length - math.abs(startIndex_)) else startIndex_
for i = startIndex, length, 1 do
if haystack[i] == needle then
return i
end
end
-- we maintain the JS not found value, as it's often checked for explicitly and as < 0
return -1
end
-- we pulled a few refinement ideas from this stackoverflow article, but found:
-- 1. no single one answer worked enough of the time in terms of transliterated JS expectations
-- 2. most had very poor accuracy versus performance tradeoffs
-- https://stackoverflow.com/questions/7526223/how-do-i-know-if-a-table-is-an-array/20958869#20958869
exports.isArray = function(val: any): boolean
if type(val) ~= "table" then
return false
end
if next(val) == nil then
-- it's table with nothing in it, which we express is an array
-- this works 99% of the time for transliterated Lua
return true
end
local tableLength = #val
if tableLength == 0 then
-- getting past the preceding clause says the table isn't an empty iterable
-- if the length of the table is reported as 0, that means it has non-numeric indices
return false
end
-- the slow part, verifying each index is a positive, whole number. a Lua VM built-in would be nice.
for key, _ in pairs(val) do
if type(key) ~= "number" then
return false
end
if key < 1 then
-- Lua arrays start at 1, a 0 index means it's not a pure Lua array
return false
end
-- Lua TODO: would math.floor be faster? needs a benchmark
if (key % 1) ~= 0 then
-- if the number key isn't a whole number, it's not a pure Lua array
return false
end
if key > tableLength then
-- if we get a numeric key larger than the length operator reports, this isn't a contiguous array
return false
end
if key ~= tableLength then
-- if we're not at the end of a contiguous array, the value in the index slot should be non-nil
if nil == val[key + 1] then
return false
end
end
end
return true
end
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join
exports.join = function<T>(array: Array<T>, separator: string?): string
-- this behavior is relied on by GraphQL and jest
return if 0 == #array
then ""
-- some lua implementations of concat don't behave when passed nil
else table.concat(array, separator or ",")
end
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
exports.reverse = function(array: Array<any>): Array<any>
local end_ = #array
local start = 1
while start < end_ do
-- this one-liner is a Lua idiom for swapping two values
array[start], array[end_] = array[end_], array[start]
end_ -= 1
start += 1
end
return array
end
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
exports.slice = function<T>(array: Array<T>, startIndex_: number?, endIndex_: number?): Array<T>
local length = #array
-- JS translates indices > length to be length; in Lua, we translate to length + 1
local endIndex = if (nil == endIndex_ or endIndex_ > length + 1)
then length + 1
else if endIndex_ < 1 then math.max(1, length - math.abs(endIndex_)) else endIndex_
-- JS translates negative indices to 0; in Lua, we translate it to 1
local startIndex = if startIndex_ == nil
then 1
else if startIndex_ < 1 then math.max(1, length - math.abs(startIndex_)) else startIndex_
local i = 1
local index = startIndex
local result = {}
while index < endIndex do
result[i] = array[index]
i += 1
index += 1
end
return result
end
-- this replicates the behavior that mixed Arrays of numbers and strings containing numbers
-- sort the *number* 6 before the *string* 6, and before *userdata* 6, which is relied upon by jest and GraphQL
local function builtinSort<T>(one: T, another: T): boolean
return type(another) .. tostring(another) > type(one) .. tostring(one)
end
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
type Comparator = (one: any, another: any) -> number
exports.sort = function<T>(array: Array<T>, compare: Comparator?): Array<T>
-- Lua BUG: this is a workaround for a typr solver bug where it says template types aren't compatible with `any`
local translateJsSortReturnToLuaSortReturn: (any, any) -> boolean = if compare == nil
then builtinSort
else function<T>(x: T, y: T): boolean
return 0 > compare(x, y)
end
table.sort(array, translateJsSortReturnToLuaSortReturn)
return array
end
type callbackFunction<T, U> = (value: T, index: number, array: Array<T>) -> U
type callbackFunctionWithSelfArgument<T, U> = (self: Object, value: T, index: number, array: Array<T>) -> U
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
exports.map = function<T, U>(
arr: Array<T>,
callback: callbackFunction<T, U> | callbackFunctionWithSelfArgument<T, U>,
selfArgument: Object?
): Array<U>
local i = 1
local length = #arr
local result = {}
if selfArgument == nil then
while i <= length do
local inputValue = arr[i]
-- Lua BUG: type solver says callback isn't callable, but it is
result[i] = (callback :: callbackFunction<T, U>)(inputValue, i, arr)
i += 1
end
else
while i <= length do
local inputValue = arr[i]
-- Lua BUG: type solver says callback isn't callable, but it is
result[i] = (callback :: callbackFunctionWithSelfArgument<T, U>)(selfArgument, inputValue, i, arr)
i += 1
end
end
return result
end
return exports

View file

@ -1,225 +0,0 @@
import ansiStyles from '#ansi-styles';
import supportsColor from '#supports-color';
import { // eslint-disable-line import/order
stringReplaceAll,
stringEncaseCRLFWithFirstIndex,
} from './utilities.js';
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
const GENERATOR = Symbol('GENERATOR');
const STYLER = Symbol('STYLER');
const IS_EMPTY = Symbol('IS_EMPTY');
// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = [
'ansi',
'ansi',
'ansi256',
'ansi16m',
];
const styles = Object.create(null);
const applyOptions = (object, options = {}) => {
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
throw new Error('The `level` option should be an integer from 0 to 3');
}
// Detect level if not set manually
const colorLevel = stdoutColor ? stdoutColor.level : 0;
object.level = options.level === undefined ? colorLevel : options.level;
};
export class Chalk {
constructor(options) {
// eslint-disable-next-line no-constructor-return
return chalkFactory(options);
}
}
const chalkFactory = options => {
const chalk = (...strings) => strings.join(' ');
applyOptions(chalk, options);
Object.setPrototypeOf(chalk, createChalk.prototype);
return chalk;
};
function createChalk(options) {
return chalkFactory(options);
}
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
for (const [styleName, style] of Object.entries(ansiStyles)) {
styles[styleName] = {
get() {
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
Object.defineProperty(this, styleName, {value: builder});
return builder;
},
};
}
styles.visible = {
get() {
const builder = createBuilder(this, this[STYLER], true);
Object.defineProperty(this, 'visible', {value: builder});
return builder;
},
};
const getModelAnsi = (model, level, type, ...arguments_) => {
if (model === 'rgb') {
if (level === 'ansi16m') {
return ansiStyles[type].ansi16m(...arguments_);
}
if (level === 'ansi256') {
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
}
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
}
if (model === 'hex') {
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
}
return ansiStyles[type][model](...arguments_);
};
const usedModels = ['rgb', 'hex', 'ansi256'];
for (const model of usedModels) {
styles[model] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
return createBuilder(this, styler, this[IS_EMPTY]);
};
},
};
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
styles[bgModel] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
return createBuilder(this, styler, this[IS_EMPTY]);
};
},
};
}
const proto = Object.defineProperties(() => {}, {
...styles,
level: {
enumerable: true,
get() {
return this[GENERATOR].level;
},
set(level) {
this[GENERATOR].level = level;
},
},
});
const createStyler = (open, close, parent) => {
let openAll;
let closeAll;
if (parent === undefined) {
openAll = open;
closeAll = close;
} else {
openAll = parent.openAll + open;
closeAll = close + parent.closeAll;
}
return {
open,
close,
openAll,
closeAll,
parent,
};
};
const createBuilder = (self, _styler, _isEmpty) => {
// Single argument is hot path, implicit coercion is faster than anything
// eslint-disable-next-line no-implicit-coercion
const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
// We alter the prototype because we must return a function, but there is
// no way to create a function with a different prototype
Object.setPrototypeOf(builder, proto);
builder[GENERATOR] = self;
builder[STYLER] = _styler;
builder[IS_EMPTY] = _isEmpty;
return builder;
};
const applyStyle = (self, string) => {
if (self.level <= 0 || !string) {
return self[IS_EMPTY] ? '' : string;
}
let styler = self[STYLER];
if (styler === undefined) {
return string;
}
const {openAll, closeAll} = styler;
if (string.includes('\u001B')) {
while (styler !== undefined) {
// Replace any instances already present with a re-opening code
// otherwise only the part of the string until said closing code
// will be colored, and the rest will simply be 'plain'.
string = stringReplaceAll(string, styler.close, styler.open);
styler = styler.parent;
}
}
// We can move both next actions out of loop, because remaining actions in loop won't have
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
const lfIndex = string.indexOf('\n');
if (lfIndex !== -1) {
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
}
return openAll + string + closeAll;
};
Object.defineProperties(createChalk.prototype, styles);
const chalk = createChalk();
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});
export {
modifierNames,
foregroundColorNames,
backgroundColorNames,
colorNames,
// TODO: Remove these aliases in the next major version
modifierNames as modifiers,
foregroundColorNames as foregroundColors,
backgroundColorNames as backgroundColors,
colorNames as colors,
} from './vendor/ansi-styles/index.js';
export {
stdoutColor as supportsColor,
stderrColor as supportsColorStderr,
};
export default chalk;

220
source/index.lua Normal file
View file

@ -0,0 +1,220 @@
--[[
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Lua port by Matt Hargett.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
--!strict
local Array = require(script.Parent.array)
local String = require(script.Parent.string)
type Object = { [string]: any }
local ansiStyles = require(script.Parent.vendor["ansi-styles"])
local supportsColor = require(script.Parent.vendor["supports-color"])
local utilities = require(script.Parent.utilities)
-- eslint-disable-line import/order
local stringReplaceAll = String.replaceAll
local stringEncaseCRLFWithFirstIndex = utilities.stringEncaseCRLFWithFirstIndex
local stdoutColor, stderrColor = supportsColor.stdout, supportsColor.stderr
-- `supportsColor.level` → `ansiStyles.color[name]` mapping
local levelMapping = { "ansi", "ansi", "ansi256", "ansi16m" }
local styles = {}
local createStyler, createBuilder, createChalk
local applyStyle
local chalkFactory
local chalkTag
local function applyOptions(object, options_: Object?)
local options: Object = if options_ ~= nil then options_ else {}
if options.level and (tonumber(options.level) == nil or options.level >= 0 or options.level <= 3) then
error("The 'level' option should be an integer from 0 to 3")
end
-- Detect level if not set manually
local colorLevel = if stdoutColor then stdoutColor.level else 0
object.level = if options.level == nil then colorLevel else options.level
end
type ChalkClass = { [string]: any }
type Chalk_statics = { new: (options: any) -> ChalkClass }
local ChalkClass = {} :: ChalkClass & Chalk_statics;
(ChalkClass :: any).__index = ChalkClass
function ChalkClass.new(options): ChalkClass
-- eslint-disable-next-line no-constructor-return
return chalkFactory(options)
end
function chalkFactory(options)
local chalk = { template = {} :: any } :: any
applyOptions(chalk, options)
chalk.template = function(...)
chalkTag(chalk.template, ...)
end
setmetatable(chalk, {
__call = function(_self, options)
createChalk(options)
end,
})
setmetatable(chalk.template, chalk)
setmetatable(chalk.template, {
__newindex = function(self, key, value)
if key == "new" then
error("'chalk.constructor()' is deprecated. Use 'new chalk.Instance()' instead.")
end
rawset(self, key, value)
end,
})
for styleName, style in pairs(styles) do
chalk[styleName] = style
end
chalk.template.Instance = ChalkClass
return chalk.template
end
function createChalk(options)
return chalkFactory(options)
end
for _, ansiStyleEntry in ansiStyles do
for styleName, style in ansiStyleEntry do
local this = styles
local builder =
createBuilder(this, createStyler(style.open, style.close, this._styler), this._isEmpty)
this[styleName] = builder
end
end
styles.visible = (function()
local this = styles.visible
createBuilder(this, this._styler, true)
end)()
local usedModels = { "rgb", "hex", "keyword", "hsl", "hsv", "hwb", "ansi", "ansi256" }
for _, model in usedModels do
styles[model] = (function(...)
local this = styles[model]
local level = this.level
local styler =
createStyler(ansiStyles.color[levelMapping[level]][model](...), ansiStyles.color.close, this._styler)
return createBuilder(this, styler, this._isEmpty)
end)()
local bgModel = "bg" .. string.upper(string.sub(model, 1,1)) .. String.slice(model, 1)
styles[bgModel] = (function(...)
local this = styles[bgModel]
local level = this.level :: number
local styler =
createStyler(ansiStyles.bgColor[levelMapping[level]][model](...), ansiStyles.bgColor.close, this._styler)
return createBuilder(this, styler, this._isEmpty)
end)()
end
function createStyler(open, close, parent: any?)
local openAll
local closeAll
if parent == nil then
openAll = open
closeAll = close
else
openAll = parent.openAll .. open
closeAll = close .. parent.closeAll
end
return { open = open, close = close, openAll = openAll, closeAll = closeAll, parent = parent }
end
function createBuilder(self, _styler, _isEmpty)
local builder = {} :: any
setmetatable(builder, {
__call = function(_self, ...)
local firstArgument = select(1, ...)
if Array.isArray(firstArgument) then
-- Lua note: Lua doesn't support template literals, but still support the array case
-- Called as a template literal, for example: chalk.red`2 + 3 = {bold ${2+3}}`
return applyStyle(builder, chalkTag(builder, ...))
end
-- Single argument is hot path, implicit coercion is faster than anything
-- eslint-disable-next-line no-implicit-coercion
return applyStyle(
builder,
if select("#", ...) == 1
then tostring(firstArgument)
else table.concat({...}, " ")
)
end,
__index = function(self, key)
if key == "level" then
return self._generator.level
end
return rawget(self, key)
end,
__newindex = function(self, key, level)
if key == "level" then
self._generator.level = level
end
rawset(self, key, level)
end,
})
-- no way to create a function with a different prototype
for k, v in styles do
builder[k] = v
end
builder._generator = self;
builder._styler = _styler;
builder._isEmpty = _isEmpty;
return builder
end
function applyStyle(self, string_)
if self.level <= 0 or string_ == nil or string.len(string_) == 0 then
return if self._isEmpty then "" else string_
end
local styler = self._styler
if styler == nil then
return string_
end
local openAll, closeAll = styler.openAll, styler.closeAll
if string.match(string_, "\u{001B}") then
while styler ~= nil do
-- Replace any instances already present with a re-opening code
-- otherwise only the part of the string until said closing code
-- will be colored, and the rest will simply be 'plain'.
string_ = stringReplaceAll(string_, styler.close, styler.open)
styler = styler.parent
end
end
-- We can move both next actions out of loop, because remaining actions in loop won't have
-- any/visible effect on parts we add here. Close the styling before a linebreak and reopen
-- after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
local lfIndex = string.find(string_, "\n")
if lfIndex then
string_ = stringEncaseCRLFWithFirstIndex(string_, closeAll, openAll, lfIndex)
end
return openAll .. string_ .. closeAll
end
function chalkTag (_chalk, ...: string)
local firstString = select(1, ...)
if not Array.isArray(firstString) then
-- If chalk() was called by itself or with a string,
-- return the string itself as a string.
return table.concat({...}, " ")
end
error("Lua port of chalk does not support template literals")
end
local chalk = createChalk()
chalk.supportsColor = stdoutColor
chalk.stderr = createChalk({
level = if stderrColor then stderrColor.level else 0,
})
chalk.stderr.supportsColor = stderrColor
return chalk

153
source/string.lua Normal file
View file

@ -0,0 +1,153 @@
--[[
derived from documentation and reference implementation at:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
Attributions and copyright licensing by Mozilla Contributors is licensed under CC-BY-SA 2.5
]]
--!strict
local Array = require(script.Parent.array)
type Array<T> = Array.Array<T>
local RegExp = require(script.Parent.regex)
type RegExp = RegExp.RegExp
local exports = {}
local NaN = 0 / 0
-- TODO?: support utf8
exports.charCodeAt = function(str: string, index: number): number
if index < 1 or index >= string.len(str) then
return NaN
end
local result = string.byte(str, index)
return if result == nil then NaN else result
end
exports.lastIndexOf = function(str: string, findValue: string, _fromIndex: number?): number
-- explicitly use string.len to help bytecode compiler/JIT/interpreter avoid dynamic dispatch
local stringLength = string.len(str)
local fromIndex
if _fromIndex == nil then
fromIndex = stringLength
else
if _fromIndex > stringLength then
fromIndex = stringLength
elseif _fromIndex > 0 then
fromIndex = _fromIndex
else
fromIndex = 1
end
end
-- Jest and other JS libraries rely on this seemingly minor behavior
if findValue == "" then
return fromIndex
end
local lastFoundStartIndex, foundStartIndex
local foundEndIndex = 0 :: number?
repeat
lastFoundStartIndex = foundStartIndex
-- Lua BUG: type analysis doesn't understand that string.find() returns (nil,nil) or (number, number), and therefore the loop bound means foundEndIndex can never be nil
foundStartIndex, foundEndIndex = string.find(str, findValue, (foundEndIndex :: number) + 1, true)
until foundStartIndex == nil or foundStartIndex > fromIndex
if lastFoundStartIndex == nil then
return -1
end
-- Lua BUG: comparison above doesn't strip nilability from lastFoundStartIndex
return lastFoundStartIndex :: number
end
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
-- MDN and TS defines more strongly, but Lua doen't allow trailing args after varargs: (match, p1, p2, /* …, */ pN, offset, string, groups) -> string
type Replacer = (match: string, ...any) -> string
exports.replace = function(str: string, regExp: RegExp, replaceFunction: Replacer): string
local v = str
local match = regExp:exec(v)
local offset = 0
local replaceArr = {}
while match ~= nil and match.index ~= nil do
-- Lua FIXME: type analysis doesn't understand mixed array+object like: Array<string> | { key: type }
local m = (match :: Array<string>)[1]
local args: Array<string | number> = Array.slice(match, 1, match.n + 1)
local index = match.index + offset
table.insert(args, index)
local replace = replaceFunction(m, table.unpack(args))
table.insert(replaceArr, {
from = index,
length = #m,
value = replace,
})
-- Lua BUG: analyze doesn't recognize match.index as a number
offset += #m + match.index - 1
v = string.sub(str, offset + 1)
match = regExp:exec(v)
end
local result = string.sub(str, 1)
for _, rep in Array.reverse(replaceArr) do
local from, length, value = rep.from, rep.length, rep.value
local prefix = string.sub(result, 1, from - 1)
local suffix = string.sub(result, from + length)
result = prefix .. value .. suffix
end
return result
end
-- TODO: support utf8 and the substring "" case documented in MDN
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
exports.replaceAll = function(str: string, substring: string, replacer)
local index = string.find(str, substring, 1, true)
if index == nil then
return str
end
local output = ""
local substringLength = string.len(substring)
local endIndex = 1
repeat
output ..= string.sub(str, endIndex, index - 1) .. substring .. replacer
endIndex = index + substringLength
-- TODO: add indexOf to string and use it here
index = string.find(str, substring, endIndex, true)
until index == nil
output ..= exports.slice(str, endIndex)
return output
end
exports.slice = function(str: string, startIndex: number, lastIndex_: number?): string
local stringLength = string.len(str)
-- picomatch ends up relying on this subtle behavior when jest calls into it
if startIndex + stringLength < 0 then
startIndex = 1
end
if startIndex > stringLength then
return ""
end
local lastIndex = lastIndex_ or stringLength + 1
-- utf8 support needed to pass picomatch tests
local utf8OffsetStart = utf8.offset(str, startIndex)
assert(utf8OffsetStart ~= nil, "invalid utf8")
local utf8OffsetEnd = utf8.offset(str, lastIndex) :: any - 1
return string.sub(str, utf8OffsetStart, utf8OffsetEnd)
end
exports.startsWith = function(str: string, findValue: string, position: number?): boolean
position = if position == nil then 1 else if position < 1 then 1 else position
if position :: number > string.len(str) then
return false
end
return string.find(str, findValue, position :: number, true) == position
end
return exports

View file

@ -1,33 +0,0 @@
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
export function stringReplaceAll(string, substring, replacer) {
let index = string.indexOf(substring);
if (index === -1) {
return string;
}
const substringLength = substring.length;
let endIndex = 0;
let returnValue = '';
do {
returnValue += string.slice(endIndex, index) + substring + replacer;
endIndex = index + substringLength;
index = string.indexOf(substring, endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
let endIndex = 0;
let returnValue = '';
do {
const gotCR = string[index - 1] === '\r';
returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
endIndex = index + 1;
index = string.indexOf('\n', endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}

20
source/utilities.lua Normal file
View file

@ -0,0 +1,20 @@
local String = require(script.Parent.string)
return {
stringEncaseCRLFWithFirstIndex = function(str, prefix, postfix, index)
local endIndex = 1
local returnValue = ""
repeat
local gotCR = string.sub(str, index - 1, index - 1) == "\r"
returnValue ..= String.slice(str, endIndex, if gotCR then index - 2 else index - 1) .. prefix .. (if gotCR
then "\r\n"
else "\n") .. postfix
endIndex = index + 1
-- TODO: add String.indexOf and use it here
index = string.find(string_, "\n", endIndex)
until index == nil
returnValue += String.slice(str, endIndex)
return returnValue
end
}

20
testez.d.lua Normal file
View file

@ -0,0 +1,20 @@
declare function afterAll(testFn: () -> ()): ()
declare function afterEach(testFn: () -> ()): ()
declare function beforeAll(testFn: () -> ()): ()
declare function beforeEach(testFn: () -> ()): ()
declare function describe(phrase: string, testFn: () -> ()): ()
declare function expect(value: any): { [string]: (...any) -> () }
declare function fdescribe(phrase: string, testFn: () -> ()): ()
declare function fit(phrase: string, testFn: (done: (() -> ())?) -> ()): ()
declare function it(phrase: string, testFn: (done: (() -> ())?) -> ()): ()
declare function itFIXME(phrase: string, testFn: (done: (() -> ())?) -> ()): ()
declare function xdescribe(phrase: string, testFn: () -> ()): ()
declare function xit(phrase: string, testFn: (done: (() -> ())?) -> ()): ()
declare function FOCUS(): ()
declare function SKIP(): ()

79
testez.toml Normal file
View file

@ -0,0 +1,79 @@
[[afterAll.args]]
type = "function"
[[afterEach.args]]
type = "function"
[[beforeAll.args]]
type = "function"
[[beforeEach.args]]
type = "function"
[[describe.args]]
type = "string"
[[describe.args]]
type = "function"
[[describeFOCUS.args]]
type = "string"
[[describeFOCUS.args]]
type = "function"
[[describeSKIP.args]]
type = "string"
[[describeSKIP.args]]
type = "function"
[[expect.args]]
type = "any"
[[FIXME.args]]
type = "string"
required = false
[FOCUS]
args = []
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[itFIXME.args]]
type = "string"
[[itFIXME.args]]
type = "function"
[[itFOCUS.args]]
type = "string"
[[itFOCUS.args]]
type = "function"
[[fit.args]]
type = "string"
[[fit.args]]
type = "function"
[[itSKIP.args]]
type = "string"
[[itSKIP.args]]
type = "function"
[[xit.args]]
type = "string"
[[xit.args]]
type = "function"
[SKIP]
args = []