From f1533bfd9c479c322b0ef65a1654558521f25095 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 3 Apr 2023 16:15:27 -0700 Subject: [PATCH] First file translated to lua --- .gitignore | 46 ++++++++- default.project.json | 6 ++ foreman.toml | 6 ++ selene.toml | 7 ++ source/array.lua | 186 +++++++++++++++++++++++++++++++++++ source/index.js | 225 ------------------------------------------- source/index.lua | 220 ++++++++++++++++++++++++++++++++++++++++++ source/string.lua | 153 +++++++++++++++++++++++++++++ source/utilities.js | 33 ------- source/utilities.lua | 20 ++++ testez.d.lua | 20 ++++ testez.toml | 79 +++++++++++++++ 12 files changed, 741 insertions(+), 260 deletions(-) create mode 100644 default.project.json create mode 100644 foreman.toml create mode 100644 selene.toml create mode 100644 source/array.lua delete mode 100644 source/index.js create mode 100644 source/index.lua create mode 100644 source/string.lua delete mode 100644 source/utilities.js create mode 100644 source/utilities.lua create mode 100644 testez.d.lua create mode 100644 testez.toml diff --git a/.gitignore b/.gitignore index 6bff314..a01e9e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,46 @@ +# 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* +debug.log +/luacov.* +/lcov* +/site +/output.txt +cachegrind.out.* +callgrind.out.* + +# npm node_modules +npm-debug.log + +# yarn yarn.lock -coverage -.nyc_output +yarn-error.log + +# misc +_gh_pages +_draft +_drafts +bower_components +vendor +temp +tmp +TODO.md +package-lock.json + + diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..51d02cb --- /dev/null +++ b/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "chalk", + "tree": { + "$path": "source" + } +} diff --git a/foreman.toml b/foreman.toml new file mode 100644 index 0000000..0733f5d --- /dev/null +++ b/foreman.toml @@ -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" } \ No newline at end of file diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..7df0e31 --- /dev/null +++ b/selene.toml @@ -0,0 +1,7 @@ +std="roblox+testez" +[config] +empty_if = { comments_count = true } + +[rules] +incorrect_standard_library_use = "allow" +shadowing = "allow" diff --git a/source/array.lua b/source/array.lua new file mode 100644 index 0000000..91891fc --- /dev/null +++ b/source/array.lua @@ -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 = { [number]: T } +type Object = { [string]: any } +exports.indexOf = function(haystack: Array, 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(array: Array, 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): Array + 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(array: Array, startIndex_: number?, endIndex_: number?): Array + 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(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(array: Array, compare: Comparator?): Array + -- 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(x: T, y: T): boolean + return 0 > compare(x, y) + end + + table.sort(array, translateJsSortReturnToLuaSortReturn) + return array +end + +type callbackFunction = (value: T, index: number, array: Array) -> U +type callbackFunctionWithSelfArgument = (self: Object, value: T, index: number, array: Array) -> U +-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map +exports.map = function( + arr: Array, + callback: callbackFunction | callbackFunctionWithSelfArgument, + selfArgument: Object? +): Array + 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)(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)(selfArgument, inputValue, i, arr) + i += 1 + end + end + + return result +end + +return exports diff --git a/source/index.js b/source/index.js deleted file mode 100644 index 8bc993d..0000000 --- a/source/index.js +++ /dev/null @@ -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; diff --git a/source/index.lua b/source/index.lua new file mode 100644 index 0000000..a7f3b7e --- /dev/null +++ b/source/index.lua @@ -0,0 +1,220 @@ +--[[ + MIT License + + Copyright (c) Sindre Sorhus (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 \ No newline at end of file diff --git a/source/string.lua b/source/string.lua new file mode 100644 index 0000000..034d4cb --- /dev/null +++ b/source/string.lua @@ -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 = Array.Array +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 | { key: type } + local m = (match :: Array)[1] + local args: Array = 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 diff --git a/source/utilities.js b/source/utilities.js deleted file mode 100644 index 4366dee..0000000 --- a/source/utilities.js +++ /dev/null @@ -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; -} diff --git a/source/utilities.lua b/source/utilities.lua new file mode 100644 index 0000000..f36db8f --- /dev/null +++ b/source/utilities.lua @@ -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 +} \ No newline at end of file diff --git a/testez.d.lua b/testez.d.lua new file mode 100644 index 0000000..df8656e --- /dev/null +++ b/testez.d.lua @@ -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(): () + diff --git a/testez.toml b/testez.toml new file mode 100644 index 0000000..b8c0cee --- /dev/null +++ b/testez.toml @@ -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 = [] +