/**
* String and locale related functions.
* @module @lumjs/core/strings
*/
const {S,B,F,isObj,root,isArray,needType,needObj,console} = require('./types');
/**
* Get the locale/language string.
*
* 1. If `navigator.language` exists it will be used.
* 2. If `Intl` exists it will be used.
* 3. If neither of those exist, uses `'en-US'` as a default.
*
* @returns {string} - The locale/language string.
* @alias module:@lumjs/core/strings.getLocale
*/
function getLocale()
{
if (isObj(root.navigator) && typeof root.navigator.language === S)
{
return root.navigator.language;
}
else if (isObj(root.Intl))
{
try
{
const lang = root.Intl.DateTimeFormat().resolvedOptions().locale;
return lang;
}
catch (err)
{
console.warn("Attempt to get locale from Intl failed", err);
}
}
// A last-ditch fallback.
return 'en-US';
}
exports.getLocale = getLocale;
/**
* Make the first character of a string uppercase.
*
* @param {string} string - The input string.
* @param {boolean} [lcrest=false] Make the rest of the string lowercase?
* @param {string} [locale=getLocale()] The locale/language of the string.
*
* @returns {string} - The output string.
* @alias module:@lumjs/core/strings.ucfirst
*/
function ucfirst ([ first, ...rest ], lcrest = false, locale = getLocale())
{
first = first.toLocaleUpperCase(locale);
rest = rest.join('');
if (lcrest)
{
rest = rest.toLocaleLowerCase(locale);
}
return first + rest;
}
exports.ucfirst = ucfirst;
/**
* Make the first character of each *word* in a string uppercase.
*
* @param {string} string - The input string.
* @param {boolean} [unicode=false] Use *Unicode* words?
* Only uses simple *PCRE-style* words (`\w`) otherwise.
* @param {boolean} [lcrest=false] Make the rest of each word lowercase?
* @param {string} [locale=getLocale()] The locale/language of the string.
*
* @returns {string} - The output string.
* @alias module:@lumjs/core/strings.ucwords
*/
function ucwords(string, unicode = false, lcrest = false, locale = getLocale())
{
const regex = unicode ? /[0-9\p{L}]+/ug : /\w+/g;
return string.replace(regex, word => ucfirst(word, lcrest, locale));
}
exports.ucwords = ucwords;
/**
* Is the passed in value a valid `String.replace` search value.
*
* Only strings and `RegExp` objects are valid.
*
* @param {*} v - Value to check.
* @returns {boolean}
* @alias module:@lumjs/core/strings.isSearch
*/
function isSearch(value)
{
return (typeof value === S || value instanceof RegExp);
}
exports.isSearch = isSearch;
/**
* Is the passed in value a valid `String.replace` replacement value.
*
* Only strings and functions are valid.
*
* @param {*} v - Value to check.
* @returns {boolean}
* @alias module:@lumjs/core/strings.isReplacement
*/
function isReplacement(value)
{
return (typeof value === S || typeof value === F);
}
exports.isReplacement = isReplacement;
/**
* Apply multiple replacement rules to a string.
*
* @param {string} string - The input string.
* @param {object} replacements - Replacement rules.
*
* If this is an `Array` then each item in the array can be:
* - Another array with two items, `[find, replace]`;
* - A `boolean`, will change the current `useAll` settting.
*
* If this is any other kind of `object` then the enumerable
* property *keys* will be used as `find` strings, and the
* associated *values* will be used as the `replacement` values.
*
* @param {boolean} [useAll] Which replacement method will be used.
* If `true` we use `replaceAll()`, if `false` we use `replace()`.
* The default is `false` if `value` is an `Array`, or `true` otherwise.
* @returns {string} The output string with all replacements performed.
* @alias module:@lumjs/core/strings.replaceItems
*/
function replaceItems(string, replacements, useAll)
{
needType(S, string);
needObj(replacements);
// This will call the appropriate method.
function replace()
{
if (useAll)
string = string.replaceAll(...arguments);
else
string = string.replace(...arguments);
}
if (isArray(replacements))
{ // An array of arrays of replace/replaceAll parameters.
useAll = useAll ?? false; // Defaults to false here if it wasn't set.
for (const replacement of replacements)
{
if (isArray(replacement) && replacement.length == 2
&& isSearch(replacement[0]) && isReplacement(replacement[1]))
{ // A set of parameters for the function.
replace(...replacement);
}
else if (typeof replacement === B)
{ // A boolean will override the current useAll value.
useAll = replacement;
}
else
{ // That's not valid.
console.error("Invalid replacement value", replacement);
}
}
}
else
{ // Any other object is a map of find strings to replacement values.
useAll = useAll ?? true; // Defaults to true here if it wasn't set.
for (const find in replacements)
{
const value = replacements[find];
if (isReplacement(value))
{
replace(find, value);
}
else
{
console.error("Invalid replacement value", value);
}
}
}
return string;
}
exports.replaceItems = replaceItems;
/**
* Replace values in a string via an async function.
*
* @param {string} string - The input string.
*
* @param {(RegExp|string)} regexp - The pattern to find.
*
* If this is a `RegExp`, it *must* have the `g` flag set.
* If this is a `string` it will be converted into a `RegExp`,
* see the `String#matchAll` method for details.
*
* @param {function} replacerFunction - An `async` function.
*
* See `String#replace` for details on replacer functions.
*
* @returns {string}
*/
async function replaceAsync(string, regexp, replacerFunction)
{
const replacements = await Promise.all(
Array.from(string.matchAll(regexp),
match => replacerFunction(...match)));
let i = 0;
return string.replace(regexp, () => replacements[i++]);
}
exports.replaceAsync = replaceAsync;