Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to disable the platform locale #92

Merged
merged 3 commits into from Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion index.d.ts
Expand Up @@ -17,6 +17,8 @@ declare namespace camelcase {
/**
The locale parameter indicates the locale to be used to convert to upper/lower case according to any locale-specific case mappings. If multiple locales are given in an array, the best available locale is used.

Setting `locale: false` ignores the platform locale and uses the [Unicode Default Case Conversion](https://unicode-org.github.io/icu/userguide/transforms/casemappings.html#simple-single-character-case-mapping) algorithm.

Default: The host environment’s current locale.

@example
Expand All @@ -33,7 +35,7 @@ declare namespace camelcase {
//=> 'loremİpsum'
```
*/
readonly locale?: string | readonly string[];
readonly locale?: false | string | readonly string[];
}
}

Expand Down
37 changes: 22 additions & 15 deletions index.js
Expand Up @@ -10,7 +10,7 @@ const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');

const preserveCamelCase = (string, locale) => {
const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
let isLastCharLower = false;
let isLastCharUpper = false;
let isLastLastCharUpper = false;
Expand All @@ -30,27 +30,27 @@ const preserveCamelCase = (string, locale) => {
isLastCharUpper = false;
isLastCharLower = true;
} else {
isLastCharLower = character.toLocaleLowerCase(locale) === character && character.toLocaleUpperCase(locale) !== character;
isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
isLastLastCharUpper = isLastCharUpper;
isLastCharUpper = character.toLocaleUpperCase(locale) === character && character.toLocaleLowerCase(locale) !== character;
isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
}
}

return string;
};

const preserveConsecutiveUppercase = input => {
const preserveConsecutiveUppercase = (input, toLowerCase) => {
LEADING_CAPITAL.lastIndex = 0;

return input.replace(LEADING_CAPITAL, m1 => m1.toLowerCase());
return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
};

const postProcess = (input, options) => {
const postProcess = (input, toUpperCase) => {
SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
NUMBERS_AND_IDENTIFIER.lastIndex = 0;

return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => identifier.toLocaleUpperCase(options.locale))
.replace(NUMBERS_AND_IDENTIFIER, m => m.toLocaleUpperCase(options.locale));
return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier))
.replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
};

const camelCase = (input, options) => {
Expand All @@ -76,29 +76,36 @@ const camelCase = (input, options) => {
return '';
}

const toLowerCase = options.locale === false ?
string => string.toLowerCase() :
string => string.toLocaleLowerCase(options.locale);
const toUpperCase = options.locale === false ?
string => string.toUpperCase() :
string => string.toLocaleUpperCase(options.locale);

if (input.length === 1) {
return options.pascalCase ? input.toLocaleUpperCase(options.locale) : input.toLocaleLowerCase(options.locale);
return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
}

const hasUpperCase = input !== input.toLocaleLowerCase(options.locale);
const hasUpperCase = input !== toLowerCase(input);

if (hasUpperCase) {
input = preserveCamelCase(input, options.locale);
input = preserveCamelCase(input, toLowerCase, toUpperCase);
}

input = input.replace(LEADING_SEPARATORS, '');

if (options.preserveConsecutiveUppercase) {
input = preserveConsecutiveUppercase(input);
input = preserveConsecutiveUppercase(input, toLowerCase);
} else {
input = input.toLocaleLowerCase();
input = toLowerCase(input);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this fixes a bug "by accident" where the locale hasn't been applied previously.

}

if (options.pascalCase) {
input = input.charAt(0).toLocaleUpperCase(options.locale) + input.slice(1);
input = toUpperCase(input.charAt(0)) + input.slice(1);
}

return postProcess(input, options);
return postProcess(input, toUpperCase);
};

module.exports = camelCase;
Expand Down
16 changes: 15 additions & 1 deletion readme.md
Expand Up @@ -94,7 +94,7 @@ Preserve the consecutive uppercase characters: `foo-BAR` → `FooBAR`.

##### locale

Type: `string | string[]`\
Type: `false | string | string[]`\
Default: The host environment’s current locale.

The locale parameter indicates the locale to be used to convert to upper/lower case according to any locale-specific case mappings. If multiple locales are given in an array, the best available locale is used.
Expand All @@ -115,6 +115,20 @@ camelCase('lorem-ipsum', {locale: ['tr', 'TR', 'tr-TR']});
//=> 'loremİpsum'
```

Setting `locale: false` ignores the platform locale and uses the [Unicode Default Case Conversion](https://unicode-org.github.io/icu/userguide/transforms/casemappings.html#simple-single-character-case-mapping) algorithm:

```js
const camelCase = require('camelcase');

// On a platform with 'tr-TR'

camelCase('lorem-ipsum');
//=> 'loremİpsum'

camelCase('lorem-ipsum', {locale: false});
//=> 'loremIpsum'
```

## camelcase for enterprise

Available as part of the Tidelift Subscription.
Expand Down
35 changes: 35 additions & 0 deletions test.js
Expand Up @@ -204,8 +204,43 @@ test('camelCase with locale option', t => {
t.is(camelCase('ipsum-dolor', {pascalCase: true, locale: ['en-EN', 'en-GB']}), 'IpsumDolor');
});

test('camelCase with disabled locale', t => {
withLocaleCaseFunctionsMocked(() => {
t.is(camelCase('lorem-ipsum', {locale: false}), 'loremIpsum');
t.is(camelCase('ipsum-dolor', {pascalCase: true, locale: false}), 'IpsumDolor');
t.is(camelCase('ipsum-DOLOR', {pascalCase: true, locale: false, preserveConsecutiveUppercase: true}), 'IpsumDOLOR');
});
});

test('invalid input', t => {
t.throws(() => {
camelCase(1);
}, /Expected the input to be/);
});

/* eslint-disable no-extend-native */
const withLocaleCaseFunctionsMocked = fn => {
const throwWhenBeingCalled = () => {
throw new Error('Should not be called');
};

const toLocaleUpperCase = Object.getOwnPropertyDescriptor(String.prototype, 'toLocaleUpperCase');
const toLocaleLowerCase = Object.getOwnPropertyDescriptor(String.prototype, 'toLocaleLowerCase');

Object.defineProperty(String.prototype, 'toLocaleUpperCase', {
...toLocaleUpperCase,
value: throwWhenBeingCalled
});
Object.defineProperty(String.prototype, 'toLocaleLowerCase', {
...toLocaleLowerCase,
value: throwWhenBeingCalled
});

try {
fn();
} finally {
Object.defineProperty(String.prototype, 'toLocaleUpperCase', toLocaleUpperCase);
Object.defineProperty(String.prototype, 'toLocaleLowerCase', toLocaleLowerCase);
}
};
/* eslint-enable no-extend-native */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to separate this mocking setup from the rest of the test. I'm not sure if you agree with this style of mocking String.prototype functions. Another solution would be to spin up a separate Node process with tr-TR as locale but I thought that would be a bigger change.