mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 17:08:51 +00:00
Compare commits
2 Commits
main
...
ciro/case-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a669587119 | ||
|
|
b6dd0dcee7 |
123
packages/bun-types/bun.d.ts
vendored
123
packages/bun-types/bun.d.ts
vendored
@@ -610,6 +610,129 @@ declare module "bun" {
|
||||
*/
|
||||
function stripANSI(input: string): string;
|
||||
|
||||
/**
|
||||
* Converts a string to camelCase.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The camelCase version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.camelCase("foo bar") // "fooBar"
|
||||
* Bun.camelCase("XMLParser") // "xmlParser"
|
||||
* ```
|
||||
*/
|
||||
function camelCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to PascalCase.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The PascalCase version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.pascalCase("foo bar") // "FooBar"
|
||||
* ```
|
||||
*/
|
||||
function pascalCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to snake_case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The snake_case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.snakeCase("fooBar") // "foo_bar"
|
||||
* ```
|
||||
*/
|
||||
function snakeCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to kebab-case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The kebab-case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.kebabCase("fooBar") // "foo-bar"
|
||||
* ```
|
||||
*/
|
||||
function kebabCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to CONSTANT_CASE.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The CONSTANT_CASE version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.constantCase("fooBar") // "FOO_BAR"
|
||||
* ```
|
||||
*/
|
||||
function constantCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to dot.case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The dot.case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.dotCase("fooBar") // "foo.bar"
|
||||
* ```
|
||||
*/
|
||||
function dotCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to Capital Case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The Capital Case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.capitalCase("fooBar") // "Foo Bar"
|
||||
* ```
|
||||
*/
|
||||
function capitalCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to Train-Case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The Train-Case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.trainCase("fooBar") // "Foo-Bar"
|
||||
* ```
|
||||
*/
|
||||
function trainCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to path/case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The path/case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.pathCase("fooBar") // "foo/bar"
|
||||
* ```
|
||||
*/
|
||||
function pathCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to Sentence case.
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The Sentence case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.sentenceCase("fooBar") // "Foo bar"
|
||||
* ```
|
||||
*/
|
||||
function sentenceCase(input: string): string;
|
||||
/**
|
||||
* Converts a string to no case (lowercased words separated by spaces).
|
||||
*
|
||||
* @param input The string to convert.
|
||||
* @returns The no case version of the string.
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.noCase("fooBar") // "foo bar"
|
||||
* ```
|
||||
*/
|
||||
function noCase(input: string): string;
|
||||
|
||||
interface WrapAnsiOptions {
|
||||
/**
|
||||
* If `true`, break words in the middle if they don't fit on a line.
|
||||
|
||||
@@ -82,6 +82,8 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionBunStripANSI);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunWrapAnsi);
|
||||
}
|
||||
|
||||
#include "CaseChange.h"
|
||||
|
||||
using namespace JSC;
|
||||
using namespace WebCore;
|
||||
|
||||
@@ -932,14 +934,18 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
allocUnsafe BunObject_callback_allocUnsafe DontDelete|Function 1
|
||||
argv BunObject_lazyPropCb_wrap_argv DontDelete|PropertyCallback
|
||||
build BunObject_callback_build DontDelete|Function 1
|
||||
camelCase jsFunctionBunCamelCase DontDelete|Function 1
|
||||
capitalCase jsFunctionBunCapitalCase DontDelete|Function 1
|
||||
concatArrayBuffers functionConcatTypedArrays DontDelete|Function 3
|
||||
connect BunObject_callback_connect DontDelete|Function 1
|
||||
constantCase jsFunctionBunConstantCase DontDelete|Function 1
|
||||
cwd BunObject_lazyPropCb_wrap_cwd DontEnum|DontDelete|PropertyCallback
|
||||
color BunObject_callback_color DontDelete|Function 2
|
||||
deepEquals functionBunDeepEquals DontDelete|Function 2
|
||||
deepMatch functionBunDeepMatch DontDelete|Function 2
|
||||
deflateSync BunObject_callback_deflateSync DontDelete|Function 1
|
||||
dns constructDNSObject ReadOnly|DontDelete|PropertyCallback
|
||||
dotCase jsFunctionBunDotCase DontDelete|Function 1
|
||||
enableANSIColors BunObject_lazyPropCb_wrap_enableANSIColors DontDelete|PropertyCallback
|
||||
env constructEnvObject ReadOnly|DontDelete|PropertyCallback
|
||||
escapeHTML functionBunEscapeHTML DontDelete|Function 2
|
||||
@@ -954,6 +960,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
indexOfLine BunObject_callback_indexOfLine DontDelete|Function 1
|
||||
inflateSync BunObject_callback_inflateSync DontDelete|Function 1
|
||||
inspect BunObject_lazyPropCb_wrap_inspect DontDelete|PropertyCallback
|
||||
kebabCase jsFunctionBunKebabCase DontDelete|Function 1
|
||||
isMainThread constructIsMainThread ReadOnly|DontDelete|PropertyCallback
|
||||
jest BunObject_callback_jest DontEnum|DontDelete|Function 1
|
||||
listen BunObject_callback_listen DontDelete|Function 1
|
||||
@@ -961,7 +968,10 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
main bunObjectMain DontDelete|CustomAccessor
|
||||
mmap BunObject_callback_mmap DontDelete|Function 1
|
||||
nanoseconds functionBunNanoseconds DontDelete|Function 0
|
||||
noCase jsFunctionBunNoCase DontDelete|Function 1
|
||||
openInEditor BunObject_callback_openInEditor DontDelete|Function 1
|
||||
pascalCase jsFunctionBunPascalCase DontDelete|Function 1
|
||||
pathCase jsFunctionBunPathCase DontDelete|Function 1
|
||||
origin BunObject_lazyPropCb_wrap_origin DontEnum|ReadOnly|DontDelete|PropertyCallback
|
||||
version_with_sha constructBunVersionWithSha DontEnum|ReadOnly|DontDelete|PropertyCallback
|
||||
password constructPasswordObject DontDelete|PropertyCallback
|
||||
@@ -982,6 +992,8 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
resolveSync BunObject_callback_resolveSync DontDelete|Function 1
|
||||
revision constructBunRevision ReadOnly|DontDelete|PropertyCallback
|
||||
semver BunObject_lazyPropCb_wrap_semver ReadOnly|DontDelete|PropertyCallback
|
||||
sentenceCase jsFunctionBunSentenceCase DontDelete|Function 1
|
||||
snakeCase jsFunctionBunSnakeCase DontDelete|Function 1
|
||||
sql defaultBunSQLObject DontDelete|PropertyCallback
|
||||
postgres defaultBunSQLObject DontDelete|PropertyCallback
|
||||
SQL constructBunSQLObject DontDelete|PropertyCallback
|
||||
@@ -997,6 +1009,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
stdout BunObject_lazyPropCb_wrap_stdout DontDelete|PropertyCallback
|
||||
stringWidth Generated::BunObject::jsStringWidth DontDelete|Function 2
|
||||
stripANSI jsFunctionBunStripANSI DontDelete|Function 1
|
||||
trainCase jsFunctionBunTrainCase DontDelete|Function 1
|
||||
wrapAnsi jsFunctionBunWrapAnsi DontDelete|Function 3
|
||||
Terminal BunObject_lazyPropCb_wrap_Terminal DontDelete|PropertyCallback
|
||||
unsafe BunObject_lazyPropCb_wrap_unsafe DontDelete|PropertyCallback
|
||||
|
||||
355
src/bun.js/bindings/CaseChange.cpp
Normal file
355
src/bun.js/bindings/CaseChange.cpp
Normal file
@@ -0,0 +1,355 @@
|
||||
#include "root.h"
|
||||
#include "CaseChange.h"
|
||||
|
||||
#include <unicode/uchar.h>
|
||||
#include <unicode/utf16.h>
|
||||
#include <wtf/text/StringBuilder.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
|
||||
namespace Bun {
|
||||
|
||||
using namespace JSC;
|
||||
using namespace WTF;
|
||||
|
||||
enum class CaseType {
|
||||
Camel,
|
||||
Pascal,
|
||||
Snake,
|
||||
Kebab,
|
||||
Constant,
|
||||
Dot,
|
||||
Capital,
|
||||
Train,
|
||||
Path,
|
||||
Sentence,
|
||||
No
|
||||
};
|
||||
|
||||
enum class CharClass {
|
||||
Lower,
|
||||
Upper,
|
||||
Digit,
|
||||
Other
|
||||
};
|
||||
|
||||
enum class WordTransform {
|
||||
Lower,
|
||||
Upper,
|
||||
Capitalize
|
||||
};
|
||||
|
||||
static inline CharClass classifyCp(char32_t c)
|
||||
{
|
||||
if (c < 0x80) {
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return CharClass::Lower;
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return CharClass::Upper;
|
||||
if (c >= '0' && c <= '9')
|
||||
return CharClass::Digit;
|
||||
return CharClass::Other;
|
||||
}
|
||||
if (u_hasBinaryProperty(c, UCHAR_UPPERCASE))
|
||||
return CharClass::Upper;
|
||||
if (u_hasBinaryProperty(c, UCHAR_ALPHABETIC))
|
||||
return CharClass::Lower;
|
||||
return CharClass::Other;
|
||||
}
|
||||
|
||||
static inline char separator(CaseType type)
|
||||
{
|
||||
switch (type) {
|
||||
case CaseType::Camel:
|
||||
case CaseType::Pascal:
|
||||
return 0;
|
||||
case CaseType::Snake:
|
||||
case CaseType::Constant:
|
||||
return '_';
|
||||
case CaseType::Kebab:
|
||||
case CaseType::Train:
|
||||
return '-';
|
||||
case CaseType::Dot:
|
||||
return '.';
|
||||
case CaseType::Capital:
|
||||
case CaseType::Sentence:
|
||||
case CaseType::No:
|
||||
return ' ';
|
||||
case CaseType::Path:
|
||||
return '/';
|
||||
}
|
||||
RELEASE_ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
static inline bool hasDigitPrefixUnderscore(CaseType type)
|
||||
{
|
||||
return type == CaseType::Camel || type == CaseType::Pascal;
|
||||
}
|
||||
|
||||
static inline WordTransform getTransform(CaseType type, size_t wordIndex)
|
||||
{
|
||||
switch (type) {
|
||||
case CaseType::Camel:
|
||||
return wordIndex == 0 ? WordTransform::Lower : WordTransform::Capitalize;
|
||||
case CaseType::Pascal:
|
||||
return WordTransform::Capitalize;
|
||||
case CaseType::Snake:
|
||||
case CaseType::Kebab:
|
||||
case CaseType::Dot:
|
||||
case CaseType::Path:
|
||||
case CaseType::No:
|
||||
return WordTransform::Lower;
|
||||
case CaseType::Constant:
|
||||
return WordTransform::Upper;
|
||||
case CaseType::Capital:
|
||||
case CaseType::Train:
|
||||
return WordTransform::Capitalize;
|
||||
case CaseType::Sentence:
|
||||
return wordIndex == 0 ? WordTransform::Capitalize : WordTransform::Lower;
|
||||
}
|
||||
RELEASE_ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
// Word boundary detection and case conversion, templated on character type.
|
||||
// For Latin1Character, each element is a codepoint.
|
||||
// For UChar, we use U16_NEXT to handle surrogate pairs.
|
||||
template<typename CharType>
|
||||
static WTF::String convertCase(CaseType type, std::span<const CharType> input)
|
||||
{
|
||||
// First pass: collect word boundaries (start/end byte offsets)
|
||||
struct WordRange {
|
||||
uint32_t start;
|
||||
uint32_t end;
|
||||
};
|
||||
|
||||
Vector<WordRange, 16> words;
|
||||
{
|
||||
bool inWord = false;
|
||||
uint32_t wordStart = 0;
|
||||
uint32_t wordEnd = 0;
|
||||
CharClass prevClass = CharClass::Other;
|
||||
CharClass prevPrevClass = CharClass::Other;
|
||||
uint32_t prevPos = 0;
|
||||
|
||||
int32_t i = 0;
|
||||
int32_t length = static_cast<int32_t>(input.size());
|
||||
|
||||
while (i < length) {
|
||||
uint32_t curPos = static_cast<uint32_t>(i);
|
||||
char32_t cp;
|
||||
|
||||
if constexpr (std::is_same_v<CharType, Latin1Character>) {
|
||||
cp = input[i];
|
||||
i++;
|
||||
} else {
|
||||
U16_NEXT(input.data(), i, length, cp);
|
||||
}
|
||||
|
||||
uint32_t curEnd = static_cast<uint32_t>(i);
|
||||
CharClass curClass = classifyCp(cp);
|
||||
|
||||
if (curClass == CharClass::Other) {
|
||||
if (inWord) {
|
||||
inWord = false;
|
||||
words.append({ wordStart, wordEnd });
|
||||
prevClass = CharClass::Other;
|
||||
prevPrevClass = CharClass::Other;
|
||||
} else {
|
||||
prevClass = CharClass::Other;
|
||||
prevPrevClass = CharClass::Other;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inWord) {
|
||||
inWord = true;
|
||||
wordStart = curPos;
|
||||
wordEnd = curEnd;
|
||||
prevPrevClass = CharClass::Other;
|
||||
prevClass = curClass;
|
||||
prevPos = curPos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rule 2: upper+upper+lower → boundary before the last upper
|
||||
if (prevPrevClass == CharClass::Upper && prevClass == CharClass::Upper && curClass == CharClass::Lower) {
|
||||
words.append({ wordStart, prevPos });
|
||||
wordStart = prevPos;
|
||||
wordEnd = curEnd;
|
||||
prevPrevClass = prevClass;
|
||||
prevClass = curClass;
|
||||
prevPos = curPos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rule 1: (lower | digit) → upper boundary
|
||||
if ((prevClass == CharClass::Lower || prevClass == CharClass::Digit) && curClass == CharClass::Upper) {
|
||||
words.append({ wordStart, wordEnd });
|
||||
wordStart = curPos;
|
||||
wordEnd = curEnd;
|
||||
prevPrevClass = CharClass::Other;
|
||||
prevClass = curClass;
|
||||
prevPos = curPos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// No boundary, extend current word
|
||||
wordEnd = curEnd;
|
||||
prevPrevClass = prevClass;
|
||||
prevClass = curClass;
|
||||
prevPos = curPos;
|
||||
}
|
||||
|
||||
// Flush last word
|
||||
if (inWord)
|
||||
words.append({ wordStart, wordEnd });
|
||||
}
|
||||
|
||||
if (words.isEmpty())
|
||||
return emptyString();
|
||||
|
||||
// Second pass: build the output string
|
||||
StringBuilder builder;
|
||||
builder.reserveCapacity(input.size() + input.size() / 4);
|
||||
|
||||
char sep = separator(type);
|
||||
|
||||
for (size_t wordIndex = 0; wordIndex < words.size(); wordIndex++) {
|
||||
auto& word = words[wordIndex];
|
||||
|
||||
// Separator between words
|
||||
if (wordIndex > 0 && sep)
|
||||
builder.append(sep);
|
||||
|
||||
// Digit-prefix underscore for camelCase/pascalCase
|
||||
if (wordIndex > 0 && hasDigitPrefixUnderscore(type)) {
|
||||
char32_t firstCp;
|
||||
if constexpr (std::is_same_v<CharType, Latin1Character>) {
|
||||
firstCp = input[word.start];
|
||||
} else {
|
||||
int32_t tmpI = word.start;
|
||||
U16_NEXT(input.data(), tmpI, static_cast<int32_t>(input.size()), firstCp);
|
||||
}
|
||||
if (firstCp >= '0' && firstCp <= '9')
|
||||
builder.append('_');
|
||||
}
|
||||
|
||||
WordTransform transform = getTransform(type, wordIndex);
|
||||
|
||||
// Iterate codepoints within the word and apply transform
|
||||
int32_t pos = word.start;
|
||||
int32_t end = word.end;
|
||||
bool isFirst = true;
|
||||
|
||||
while (pos < end) {
|
||||
char32_t cp;
|
||||
if constexpr (std::is_same_v<CharType, Latin1Character>) {
|
||||
cp = input[pos];
|
||||
pos++;
|
||||
} else {
|
||||
U16_NEXT(input.data(), pos, end, cp);
|
||||
}
|
||||
|
||||
char32_t transformed;
|
||||
switch (transform) {
|
||||
case WordTransform::Lower:
|
||||
transformed = u_tolower(cp);
|
||||
break;
|
||||
case WordTransform::Upper:
|
||||
transformed = u_toupper(cp);
|
||||
break;
|
||||
case WordTransform::Capitalize:
|
||||
transformed = isFirst ? u_toupper(cp) : u_tolower(cp);
|
||||
break;
|
||||
}
|
||||
isFirst = false;
|
||||
|
||||
builder.append(static_cast<char32_t>(transformed));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static EncodedJSValue caseChangeImpl(CaseType type, JSGlobalObject* globalObject, CallFrame* callFrame)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSValue input = callFrame->argument(0);
|
||||
if (!input.isString()) {
|
||||
throwTypeError(globalObject, scope, "Expected a string argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSString* jsStr = input.toString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
auto view = jsStr->view(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (view->isEmpty())
|
||||
return JSValue::encode(jsEmptyString(vm));
|
||||
|
||||
WTF::String result = view->is8Bit()
|
||||
? convertCase<Latin1Character>(type, view->span8())
|
||||
: convertCase<UChar>(type, view->span16());
|
||||
|
||||
return JSValue::encode(jsString(vm, WTF::move(result)));
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunCamelCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Camel, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunPascalCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Pascal, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunSnakeCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Snake, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunKebabCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Kebab, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunConstantCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Constant, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunDotCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Dot, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunCapitalCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Capital, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunTrainCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Train, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunPathCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Path, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunSentenceCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::Sentence, globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunNoCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
return caseChangeImpl(CaseType::No, globalObject, callFrame);
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
19
src/bun.js/bindings/CaseChange.h
Normal file
19
src/bun.js/bindings/CaseChange.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
|
||||
namespace Bun {
|
||||
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunCamelCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPascalCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunSnakeCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunKebabCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunConstantCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunDotCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunCapitalCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunTrainCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPathCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunSentenceCase);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunNoCase);
|
||||
|
||||
}
|
||||
604
test/js/bun/util/case-change.test.ts
Normal file
604
test/js/bun/util/case-change.test.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
camelCase,
|
||||
capitalCase,
|
||||
constantCase,
|
||||
dotCase,
|
||||
kebabCase,
|
||||
noCase,
|
||||
pascalCase,
|
||||
pathCase,
|
||||
sentenceCase,
|
||||
snakeCase,
|
||||
trainCase,
|
||||
} from "change-case";
|
||||
|
||||
type CaseFn = (input: string) => string;
|
||||
|
||||
const bunFns: Record<string, CaseFn> = {
|
||||
camelCase: Bun.camelCase,
|
||||
capitalCase: Bun.capitalCase,
|
||||
constantCase: Bun.constantCase,
|
||||
dotCase: Bun.dotCase,
|
||||
kebabCase: Bun.kebabCase,
|
||||
noCase: Bun.noCase,
|
||||
pascalCase: Bun.pascalCase,
|
||||
pathCase: Bun.pathCase,
|
||||
sentenceCase: Bun.sentenceCase,
|
||||
snakeCase: Bun.snakeCase,
|
||||
trainCase: Bun.trainCase,
|
||||
};
|
||||
|
||||
const changeCaseFns: Record<string, CaseFn> = {
|
||||
camelCase,
|
||||
capitalCase,
|
||||
constantCase,
|
||||
dotCase,
|
||||
kebabCase,
|
||||
noCase,
|
||||
pascalCase,
|
||||
pathCase,
|
||||
sentenceCase,
|
||||
snakeCase,
|
||||
trainCase,
|
||||
};
|
||||
|
||||
// Comprehensive input set covering many patterns
|
||||
const testInputs = [
|
||||
// Basic words
|
||||
"test",
|
||||
"foo",
|
||||
"a",
|
||||
"",
|
||||
|
||||
// Multi-word with various separators
|
||||
"test string",
|
||||
"test_string",
|
||||
"test-string",
|
||||
"test.string",
|
||||
"test/string",
|
||||
"test\tstring",
|
||||
|
||||
// Cased inputs
|
||||
"Test String",
|
||||
"TEST STRING",
|
||||
"TestString",
|
||||
"testString",
|
||||
"TEST_STRING",
|
||||
|
||||
// Acronyms and consecutive uppercase
|
||||
"XMLParser",
|
||||
"getHTTPSURL",
|
||||
"parseJSON",
|
||||
"simpleXML",
|
||||
"PDFLoader",
|
||||
"I18N",
|
||||
"ABC",
|
||||
"ABCdef",
|
||||
"ABCDef",
|
||||
"HTMLElement",
|
||||
"innerHTML",
|
||||
"XMLHttpRequest",
|
||||
"getURLParams",
|
||||
"isHTTPS",
|
||||
"CSSStyleSheet",
|
||||
"IOError",
|
||||
"UIKit",
|
||||
|
||||
// Numbers
|
||||
"version 1.2.10",
|
||||
"TestV2",
|
||||
"test123",
|
||||
"123test",
|
||||
"test 123 value",
|
||||
"1st place",
|
||||
"v2beta1",
|
||||
"ES6Module",
|
||||
"utf8Decode",
|
||||
"base64Encode",
|
||||
"h1Element",
|
||||
"int32Array",
|
||||
"123",
|
||||
"123 456",
|
||||
"a1b2c3",
|
||||
"test0",
|
||||
|
||||
// Multiple separators / weird spacing
|
||||
"foo___bar",
|
||||
"foo---bar",
|
||||
"foo...bar",
|
||||
"foo bar",
|
||||
" leading spaces ",
|
||||
"__private",
|
||||
"--dashed--",
|
||||
"..dotted..",
|
||||
" ",
|
||||
"\t\ttabs\t\t",
|
||||
"foo_-_bar",
|
||||
"foo.-bar",
|
||||
|
||||
// All uppercase
|
||||
"FOO_BAR_BAZ",
|
||||
"ALLCAPS",
|
||||
"FOO BAR",
|
||||
"FOO-BAR",
|
||||
"FOO.BAR",
|
||||
|
||||
// Mixed case
|
||||
"fooBarBaz",
|
||||
"FooBarBaz",
|
||||
"Foo Bar",
|
||||
"MiXeD CaSe",
|
||||
"already camelCase",
|
||||
"already PascalCase",
|
||||
"already_snake_case",
|
||||
"already-kebab-case",
|
||||
"Already Capital Case",
|
||||
"ALREADY_CONSTANT_CASE",
|
||||
|
||||
// Pre-formatted cases
|
||||
"Train-Case-Input",
|
||||
"dot.case.input",
|
||||
"path/case/input",
|
||||
"Sentence case input",
|
||||
"no case input",
|
||||
|
||||
// Single characters
|
||||
"A",
|
||||
"z",
|
||||
"Z",
|
||||
"0",
|
||||
|
||||
// Real-world identifiers
|
||||
"backgroundColor",
|
||||
"border-top-color",
|
||||
"MAX_RETRY_COUNT",
|
||||
"Content-Type",
|
||||
"X-Forwarded-For",
|
||||
"user_id",
|
||||
"getUserById",
|
||||
"class_name",
|
||||
"className",
|
||||
"is_active",
|
||||
"isActive",
|
||||
"created_at",
|
||||
"createdAt",
|
||||
"HTTPSConnection",
|
||||
"myXMLParser",
|
||||
"getDBConnection",
|
||||
"setHTTPSEnabled",
|
||||
"enableSSL",
|
||||
"useGPU",
|
||||
"readCSV",
|
||||
"parseHTML",
|
||||
"toJSON",
|
||||
"fromURL",
|
||||
"isNaN",
|
||||
"toString",
|
||||
"valueOf",
|
||||
|
||||
// Column name style inputs (SQL)
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email_address",
|
||||
"phone_number",
|
||||
"order_total",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"is_deleted",
|
||||
|
||||
// Hyphenated compound words
|
||||
"well-known",
|
||||
"read-only",
|
||||
"built-in",
|
||||
"self-contained",
|
||||
|
||||
// Strings with only separators
|
||||
"---",
|
||||
"___",
|
||||
"...",
|
||||
"///",
|
||||
"-_.-_.",
|
||||
|
||||
// Unicode (basic)
|
||||
"café latte",
|
||||
"naïve résumé",
|
||||
"hello 世界",
|
||||
|
||||
// Long strings
|
||||
"this is a much longer test string with many words to convert",
|
||||
"thisIsAMuchLongerTestStringWithManyWordsToConvert",
|
||||
"THIS_IS_A_MUCH_LONGER_TEST_STRING_WITH_MANY_WORDS_TO_CONVERT",
|
||||
];
|
||||
|
||||
const allCaseNames = Object.keys(bunFns);
|
||||
|
||||
describe("case-change", () => {
|
||||
// Main compatibility matrix: every function x every input
|
||||
for (const caseName of allCaseNames) {
|
||||
const bunFn = bunFns[caseName];
|
||||
const changeCaseFn = changeCaseFns[caseName];
|
||||
|
||||
describe(caseName, () => {
|
||||
for (const input of testInputs) {
|
||||
const expected = changeCaseFn(input);
|
||||
test(`${JSON.stringify(input)} => ${JSON.stringify(expected)}`, () => {
|
||||
expect(bunFn(input)).toBe(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cross-conversion round-trips: convert from A to B, compare both implementations
|
||||
describe("cross-conversion round-trips", () => {
|
||||
const conversions = [
|
||||
"camelCase",
|
||||
"pascalCase",
|
||||
"snakeCase",
|
||||
"kebabCase",
|
||||
"constantCase",
|
||||
"noCase",
|
||||
"dotCase",
|
||||
] as const;
|
||||
const roundTripInputs = [
|
||||
"hello world",
|
||||
"fooBarBaz",
|
||||
"FOO_BAR",
|
||||
"XMLParser",
|
||||
"getHTTPSURL",
|
||||
"test_string",
|
||||
"Test String",
|
||||
"already-kebab",
|
||||
"version 1.2.10",
|
||||
];
|
||||
|
||||
for (const input of roundTripInputs) {
|
||||
for (const from of conversions) {
|
||||
for (const to of conversions) {
|
||||
const intermediate = changeCaseFns[from](input);
|
||||
const expected = changeCaseFns[to](intermediate);
|
||||
test(`${from}(${JSON.stringify(input)}) => ${to}`, () => {
|
||||
const bunIntermediate = bunFns[from](input);
|
||||
expect(bunFns[to](bunIntermediate)).toBe(expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Double-conversion stability: converting the output again should be idempotent
|
||||
describe("idempotency", () => {
|
||||
const idempotentInputs = ["hello world", "fooBarBaz", "FOO_BAR_BAZ", "XMLParser", "test 123", "café latte"];
|
||||
|
||||
for (const caseName of allCaseNames) {
|
||||
const bunFn = bunFns[caseName];
|
||||
const changeCaseFn = changeCaseFns[caseName];
|
||||
|
||||
for (const input of idempotentInputs) {
|
||||
test(`${caseName}(${caseName}(${JSON.stringify(input)})) is idempotent`, () => {
|
||||
const once = bunFn(input);
|
||||
const twice = bunFn(once);
|
||||
const expectedOnce = changeCaseFn(input);
|
||||
const expectedTwice = changeCaseFn(expectedOnce);
|
||||
expect(once).toBe(expectedOnce);
|
||||
expect(twice).toBe(expectedTwice);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Specific per-function expected values (hardcoded, not generated)
|
||||
describe("specific expected values", () => {
|
||||
test("camelCase", () => {
|
||||
expect(Bun.camelCase("foo bar")).toBe("fooBar");
|
||||
expect(Bun.camelCase("foo-bar")).toBe("fooBar");
|
||||
expect(Bun.camelCase("foo_bar")).toBe("fooBar");
|
||||
expect(Bun.camelCase("FOO_BAR")).toBe("fooBar");
|
||||
expect(Bun.camelCase("FooBar")).toBe("fooBar");
|
||||
expect(Bun.camelCase("fooBar")).toBe("fooBar");
|
||||
expect(Bun.camelCase("")).toBe("");
|
||||
expect(Bun.camelCase("foo")).toBe("foo");
|
||||
expect(Bun.camelCase("A")).toBe("a");
|
||||
});
|
||||
|
||||
test("pascalCase", () => {
|
||||
expect(Bun.pascalCase("foo bar")).toBe("FooBar");
|
||||
expect(Bun.pascalCase("foo-bar")).toBe("FooBar");
|
||||
expect(Bun.pascalCase("foo_bar")).toBe("FooBar");
|
||||
expect(Bun.pascalCase("FOO_BAR")).toBe("FooBar");
|
||||
expect(Bun.pascalCase("fooBar")).toBe("FooBar");
|
||||
expect(Bun.pascalCase("")).toBe("");
|
||||
expect(Bun.pascalCase("foo")).toBe("Foo");
|
||||
});
|
||||
|
||||
test("snakeCase", () => {
|
||||
expect(Bun.snakeCase("foo bar")).toBe("foo_bar");
|
||||
expect(Bun.snakeCase("fooBar")).toBe("foo_bar");
|
||||
expect(Bun.snakeCase("FooBar")).toBe("foo_bar");
|
||||
expect(Bun.snakeCase("FOO_BAR")).toBe("foo_bar");
|
||||
expect(Bun.snakeCase("foo-bar")).toBe("foo_bar");
|
||||
expect(Bun.snakeCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("kebabCase", () => {
|
||||
expect(Bun.kebabCase("foo bar")).toBe("foo-bar");
|
||||
expect(Bun.kebabCase("fooBar")).toBe("foo-bar");
|
||||
expect(Bun.kebabCase("FooBar")).toBe("foo-bar");
|
||||
expect(Bun.kebabCase("FOO_BAR")).toBe("foo-bar");
|
||||
expect(Bun.kebabCase("foo_bar")).toBe("foo-bar");
|
||||
expect(Bun.kebabCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("constantCase", () => {
|
||||
expect(Bun.constantCase("foo bar")).toBe("FOO_BAR");
|
||||
expect(Bun.constantCase("fooBar")).toBe("FOO_BAR");
|
||||
expect(Bun.constantCase("FooBar")).toBe("FOO_BAR");
|
||||
expect(Bun.constantCase("foo-bar")).toBe("FOO_BAR");
|
||||
expect(Bun.constantCase("foo_bar")).toBe("FOO_BAR");
|
||||
expect(Bun.constantCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("dotCase", () => {
|
||||
expect(Bun.dotCase("foo bar")).toBe("foo.bar");
|
||||
expect(Bun.dotCase("fooBar")).toBe("foo.bar");
|
||||
expect(Bun.dotCase("FOO_BAR")).toBe("foo.bar");
|
||||
expect(Bun.dotCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("capitalCase", () => {
|
||||
expect(Bun.capitalCase("foo bar")).toBe("Foo Bar");
|
||||
expect(Bun.capitalCase("fooBar")).toBe("Foo Bar");
|
||||
expect(Bun.capitalCase("FOO_BAR")).toBe("Foo Bar");
|
||||
expect(Bun.capitalCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("trainCase", () => {
|
||||
expect(Bun.trainCase("foo bar")).toBe("Foo-Bar");
|
||||
expect(Bun.trainCase("fooBar")).toBe("Foo-Bar");
|
||||
expect(Bun.trainCase("FOO_BAR")).toBe("Foo-Bar");
|
||||
expect(Bun.trainCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("pathCase", () => {
|
||||
expect(Bun.pathCase("foo bar")).toBe("foo/bar");
|
||||
expect(Bun.pathCase("fooBar")).toBe("foo/bar");
|
||||
expect(Bun.pathCase("FOO_BAR")).toBe("foo/bar");
|
||||
expect(Bun.pathCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("sentenceCase", () => {
|
||||
expect(Bun.sentenceCase("foo bar")).toBe("Foo bar");
|
||||
expect(Bun.sentenceCase("fooBar")).toBe("Foo bar");
|
||||
expect(Bun.sentenceCase("FOO_BAR")).toBe("Foo bar");
|
||||
expect(Bun.sentenceCase("")).toBe("");
|
||||
});
|
||||
|
||||
test("noCase", () => {
|
||||
expect(Bun.noCase("foo bar")).toBe("foo bar");
|
||||
expect(Bun.noCase("fooBar")).toBe("foo bar");
|
||||
expect(Bun.noCase("FOO_BAR")).toBe("foo bar");
|
||||
expect(Bun.noCase("FooBar")).toBe("foo bar");
|
||||
expect(Bun.noCase("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
// Edge cases
|
||||
describe("edge cases", () => {
|
||||
test("empty string returns empty for all functions", () => {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName]("")).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
test("single character", () => {
|
||||
for (const ch of ["a", "A", "z", "Z", "0", "9"]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](ch)).toBe(changeCaseFns[caseName](ch));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("all separators produce empty for all functions", () => {
|
||||
for (const sep of ["---", "___", "...", " ", "\t\t", "-_.-_."]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](sep)).toBe(changeCaseFns[caseName](sep));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("numbers only", () => {
|
||||
for (const input of ["123", "0", "999", "123 456", "1.2.3"]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("mixed numbers and letters", () => {
|
||||
for (const input of [
|
||||
"test123",
|
||||
"123test",
|
||||
"test 123 value",
|
||||
"1st place",
|
||||
"v2beta1",
|
||||
"a1b2c3",
|
||||
"ES6Module",
|
||||
"utf8Decode",
|
||||
"base64Encode",
|
||||
"h1Element",
|
||||
"int32Array",
|
||||
]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("consecutive separators are collapsed", () => {
|
||||
for (const input of ["foo___bar", "foo---bar", "foo...bar", "foo bar"]) {
|
||||
expect(Bun.camelCase(input)).toBe(camelCase(input));
|
||||
expect(Bun.snakeCase(input)).toBe(snakeCase(input));
|
||||
}
|
||||
});
|
||||
|
||||
test("leading and trailing separators are stripped", () => {
|
||||
for (const input of [" foo ", "__bar__", "--baz--", "..qux.."]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("unicode strings", () => {
|
||||
for (const input of ["café latte", "naïve résumé", "hello 世界"]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("acronym splitting", () => {
|
||||
// These specifically test the upper->upper+lower boundary rule
|
||||
for (const input of [
|
||||
"XMLParser",
|
||||
"HTMLElement",
|
||||
"innerHTML",
|
||||
"XMLHttpRequest",
|
||||
"getURLParams",
|
||||
"isHTTPS",
|
||||
"CSSStyleSheet",
|
||||
"IOError",
|
||||
"UIKit",
|
||||
"HTTPSConnection",
|
||||
"myXMLParser",
|
||||
"getDBConnection",
|
||||
"setHTTPSEnabled",
|
||||
"ABCDef",
|
||||
"ABCdef",
|
||||
]) {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("digit-prefix underscore in camelCase/pascalCase", () => {
|
||||
// change-case inserts _ before digit-starting words (index > 0) in camel/pascal
|
||||
const input = "version 1.2.10";
|
||||
expect(Bun.camelCase(input)).toBe(camelCase(input));
|
||||
expect(Bun.pascalCase(input)).toBe(pascalCase(input));
|
||||
// snake/kebab/etc should NOT have the _ prefix
|
||||
expect(Bun.snakeCase(input)).toBe(snakeCase(input));
|
||||
expect(Bun.kebabCase(input)).toBe(kebabCase(input));
|
||||
});
|
||||
|
||||
test("long strings", () => {
|
||||
const long =
|
||||
"this is a much longer test string with many words to convert and it keeps going and going and going";
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](long)).toBe(changeCaseFns[caseName](long));
|
||||
}
|
||||
});
|
||||
|
||||
test("repeated single word", () => {
|
||||
expect(Bun.camelCase("foo")).toBe("foo");
|
||||
expect(Bun.pascalCase("foo")).toBe("Foo");
|
||||
expect(Bun.snakeCase("foo")).toBe("foo");
|
||||
expect(Bun.kebabCase("foo")).toBe("foo");
|
||||
expect(Bun.constantCase("foo")).toBe("FOO");
|
||||
});
|
||||
|
||||
test("single uppercase word", () => {
|
||||
expect(Bun.camelCase("FOO")).toBe(camelCase("FOO"));
|
||||
expect(Bun.pascalCase("FOO")).toBe(pascalCase("FOO"));
|
||||
expect(Bun.snakeCase("FOO")).toBe(snakeCase("FOO"));
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling
|
||||
describe("error handling", () => {
|
||||
for (const caseName of allCaseNames) {
|
||||
const fn = bunFns[caseName];
|
||||
|
||||
test(`${caseName}() with no arguments throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn()).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}(123) with number throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn(123)).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}(null) throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn(null)).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}(undefined) throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn(undefined)).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}({}) with object throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn({})).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}([]) with array throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn([])).toThrow();
|
||||
});
|
||||
|
||||
test(`${caseName}(true) with boolean throws`, () => {
|
||||
// @ts-expect-error
|
||||
expect(() => fn(true)).toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure .length property is 1
|
||||
describe("function.length", () => {
|
||||
for (const caseName of allCaseNames) {
|
||||
test(`Bun.${caseName}.length === 1`, () => {
|
||||
expect(bunFns[caseName].length).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Stress test with generated inputs
|
||||
describe("generated inputs", () => {
|
||||
// Words joined with various separators
|
||||
const words = ["foo", "bar", "baz", "qux"];
|
||||
const separators = [" ", "_", "-", ".", "/", " ", "__", "--"];
|
||||
|
||||
for (const sep of separators) {
|
||||
const input = words.join(sep);
|
||||
test(`words joined by ${JSON.stringify(sep)}: ${JSON.stringify(input)}`, () => {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Various camelCase-style inputs
|
||||
const camelInputs = [
|
||||
"oneTwoThree",
|
||||
"OneTwoThree",
|
||||
"oneTWOThree",
|
||||
"ONETwoThree",
|
||||
"oneTwo3",
|
||||
"one2Three",
|
||||
"one23",
|
||||
"oneABCTwo",
|
||||
];
|
||||
|
||||
for (const input of camelInputs) {
|
||||
test(`camelCase-style: ${JSON.stringify(input)}`, () => {
|
||||
for (const caseName of allCaseNames) {
|
||||
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user