topkat-utils
Version:
A comprehensive collection of TypeScript/JavaScript utility functions for common programming tasks. Includes validation, object manipulation, date handling, string formatting, and more. Zero dependencies, fully typed, and optimized for performance.
296 lines • 13.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStringAsNumber = exports.parseStringAsBoolean = exports.parseStringVariable = exports.upperCase = exports.lowerCase = exports.nbOccurenceInString = exports.cln = exports.miniTemplater = exports.pathJoinSafe = exports.urlPathJoin = exports.generateObjectId = exports.generateToken = exports.convertAccentedCharacters = exports.getValuesBetweenStrings = exports.getValuesBetweenSeparator = exports.camelCaseToWords = exports.capitalize1st = exports.titleCase = exports.pascalCase = exports.kebabCase = exports.snakeCase = exports.camelCaseify = exports.camelCase = void 0;
//----------------------------------------
// STRING UTILS
//----------------------------------------
const error_utils_1 = require("./error-utils");
const isset_1 = require("./isset");
const getWordBits = (wb) => Array.isArray(wb[0]) ? wb[0] : wb;
/**Eg: camelCase('hello', 'world') => 'helloWorld' */
function camelCase(...wordBits) {
const wordBitsReal = getWordBits(wordBits);
return wordBitsReal.filter(e => e).map((w, i) => i === 0 ? w.toLowerCase() : capitalize1st(w, true)).join('');
}
exports.camelCase = camelCase;
/** Replace 'hello-world', 'hello World', 'hello_World', 'helloWorld' => 'helloWorld' */
function camelCaseify(word) {
const wordBitsReal = word.replace(/([A-Z])/g, ' $1').split(/[- _]/g);
return camelCase(wordBitsReal);
}
exports.camelCaseify = camelCaseify;
/**Eg: snake_case
* trimmed but not lowerCased
*/
function snakeCase(...wordBits) {
const wordBitsReal = getWordBits(wordBits);
return wordBitsReal.filter(e => e).map(w => w.trim()).join('_');
}
exports.snakeCase = snakeCase;
/**Eg: kebab-case
* trimmed AND lowerCased
* undefined, null are removed
*/
function kebabCase(...wordBits) {
const wordBitsReal = getWordBits(wordBits);
return wordBitsReal.filter(e => e).map(w => w.trim().toLowerCase()).join('-');
}
exports.kebabCase = kebabCase;
/**Eg: PascalCase
* undefined, null are removed
*/
function pascalCase(...wordBits) {
const wordBitsReal = getWordBits(wordBits);
return wordBitsReal.filter(e => e).map(w => capitalize1st(w, true)).join('');
}
exports.pascalCase = pascalCase;
/**Eg: Titlecase
* undefined, null are removed
*/
function titleCase(...wordBits) {
const wordBitsReal = getWordBits(wordBits);
return capitalize1st(wordBitsReal.filter(e => e).map(w => w.trim()).join(''), true);
}
exports.titleCase = titleCase;
function capitalize1st(str = '', lowercaseTheRest = false) { return str ? str[0].toUpperCase() + (lowercaseTheRest ? str.toLowerCase() : str).slice(1) : str; }
exports.capitalize1st = capitalize1st;
function camelCaseToWords(str) {
return str ? str.trim().replace(/([A-Z])/g, '-$1').toLowerCase().split('-') : [];
}
exports.camelCaseToWords = camelCaseToWords;
/** GIVEN A STRING '{ blah;2}, ['nested,(what,ever)']' AND A SEPARATOR ",""
* This will return the content separated by first level of separators
* @return ["{ blah;2}", "['nested,(what,ever)']"]
*/
function getValuesBetweenSeparator(str, separator, removeTrailingSpaces = true) {
(0, error_utils_1.err500IfEmptyOrNotSet)({ separator, str });
const { outer } = getValuesBetweenStrings(str, separator, undefined, undefined, undefined, removeTrailingSpaces);
return outer;
}
exports.getValuesBetweenSeparator = getValuesBetweenSeparator;
/** GIVEN A STRING "a: [ 'str', /[^]]/, '[aa]]]str', () => [ nestedArray ] ], b: ['arr']"
* @return matching: [ "'str', /[^]]/, '[aa]]]str', () => [ nestedArray ]", "'arr'" ], between: [ "a:", ", b: " ]
* @param str base string
* @param openingOrSeparator opening character OR separator if closing not set
* @param closing
* @param ignoreBetweenOpen default ['\'', '`', '"', '/'], when reaching an opening char, it will ignore all until it find the corresponding closing char
* @param ignoreBetweenClose default ['\'', '`', '"', '/'] list of corresponding closing chars
*/
function getValuesBetweenStrings(str, openingOrSeparator, closing, ignoreBetweenOpen = ['\'', '`', '"', '/'], ignoreBetweenClose = ['\'', '`', '"', '/'], removeTrailingSpaces = true) {
(0, error_utils_1.err500IfEmptyOrNotSet)({ openingOrSeparator, str });
str = str.replace(/<</g, '§§"').replace(/>>/g, '"§§');
const arrayValues = [];
const betweenArray = [];
let level = 0;
let ignoreUntil = false;
let actualValue = '';
let precedingChar = '';
let separatorMode = false;
if (!closing)
separatorMode = true;
const separator = separatorMode ? openingOrSeparator : false;
const openingChars = separatorMode ? ['(', '{', '['] : [openingOrSeparator];
const closingChars = separatorMode ? [')', '}', ']'] : [closing];
const pushActualValue = () => {
if (level === 0)
betweenArray.push(removeTrailingSpaces ? actualValue.replace(/(?:^\s+|\s+$)/g, '') : actualValue);
else
arrayValues.push(removeTrailingSpaces ? actualValue.replace(/(?:^\s+|\s+$)/g, '') : actualValue);
actualValue = '';
};
str.split('').forEach(char => {
// handle unwanted nested structure like characters in a strings that may be a unmatched closing / opening character
// Eg: {'azer}aze'}
if (ignoreUntil && char === ignoreUntil && precedingChar !== '\\')
ignoreUntil = false;
else if (ignoreUntil && char !== ignoreUntil)
ignoreUntil = true;
else if (ignoreBetweenOpen.includes(char)) {
const indexChar = ignoreBetweenOpen.findIndex(char2 => char2 === char);
ignoreUntil = ignoreBetweenClose[indexChar];
}
else if (openingChars.includes(char)) {
// handle nested structures
if (!separatorMode && level === 0)
pushActualValue();
level++;
if (!separatorMode)
return;
}
else if (closingChars.includes(char)) {
// handle nested structures
if (!separatorMode && level === 1)
pushActualValue();
level--;
}
else if (separatorMode && level === 0 && char === separator) {
// SEPARATOR MODE
pushActualValue();
return;
}
actualValue += char;
precedingChar = char;
});
pushActualValue();
const replaceValz = arr => arr.map(v => v.replace(/§§"/g, '<<').replace(/"§§/g, '>>')).filter(v => v);
return { inner: replaceValz(arrayValues), outer: replaceValz(betweenArray) };
}
exports.getValuesBetweenStrings = getValuesBetweenStrings;
/** Remove accentued character from string and eventually special chars and numbers
* @param {String} str input string
* @param {Object} config { removeSpecialChars: false, removeNumbers: false, removeSpaces: false }
* @returns String with all accentued char replaced by their non accentued version + config formattting
*/
function convertAccentedCharacters(str, config = {}) {
let output = str
.trim()
.replace(/[àáâãäå]/g, 'a')
.replace(/[ÀÁÂÃÄÅ]/g, 'A')
.replace(/ç/g, 'c')
.replace(/Ç/g, 'C')
.replace(/[èéêë]/g, 'e')
.replace(/[ÈÉÊË]/g, 'E')
.replace(/[ìíîï]/g, 'i')
.replace(/[ÌÍÎÏ]/g, 'I')
.replace(/[ôö]/g, 'o')
.replace(/[ÔÖ]/g, 'O')
.replace(/[ùúû]/g, 'u')
.replace(/[ÙÚÛ]/g, 'U');
if (config.removeNumbers === true)
output = output.replace(/\d+/g, '');
if (config.removeSpecialChars === true)
output = output.replace(/[^A-Za-z0-9 ]/g, '');
if (config.removeSpaces === true)
output = output.replace(/\s+/g, '');
return output;
}
exports.convertAccentedCharacters = convertAccentedCharacters;
let generatedTokens = []; // cache to avoid collision
let lastTs = Date.now();
/** minLength 8 if unique
* @param {Number} length default: 20
* @param {Boolean} unique default: true. Generate a real unique token base on the date. min length will be min 8 in this case
* @param {string} mode one of ['alphanumeric', 'hexadecimal']
* NOTE: to generate a mongoDB Random Id, use the params: 24, true, 'hexadecimal'
*/
function generateToken(length = 20, unique = true, mode = 'alphanumeric') {
const charConvNumeric = mode === 'alphanumeric' ? 36 : 16;
if (unique && length < 8)
throw new Error('generateToken can not be used with less than 8 characters when unique is set to true');
let token;
let tokenTs;
do {
tokenTs = Date.now();
token = unique ? tokenTs.toString(charConvNumeric) : '';
while (token.length < length)
token += Math.random().toString(charConvNumeric).substr(2, 1); // char alphaNumeric aléatoire
} while (generatedTokens.includes(token));
if (lastTs < tokenTs) {
generatedTokens = []; // reset generated token on new timestamp because cannot collide
lastTs = Date.now();
}
generatedTokens.push(token);
return token;
}
exports.generateToken = generateToken;
function generateObjectId() {
return generateToken(24, true, 'hexadecimal');
}
exports.generateObjectId = generateObjectId;
/** Useful to join differents bits of url with normalizing slashes
* * urlPathJoin('https://', 'www.kikou.lol/', '/user', '//2//') => https://www.kikou.lol/user/2
* * urlPathJoin('http:/', 'kikou.lol') => https://www.kikou.lol
*/
function urlPathJoin(...bits) {
return bits
.join('/')
.replace(/\/+/g, '/') // replace double slash
.replace(/(^\/|\/$)/g, '') // remove starting and trailing slash
.replace(/(https?:)\/\/?/, '$1//'); // make sure there is 2 slashes after http(s)
}
exports.urlPathJoin = urlPathJoin;
/** @deprecated use urlPathJoin instead // file path using ONLY SLASH and not antislash on windows. Remove also starting and trailing slashes */
function pathJoinSafe(...pathBits) {
return pathBits
.join('/')
.replace(/\/+/g, '/') // replace double slash
.replace(/(^\/|\/$)/g, ''); // remove starting and trailing slash
}
exports.pathJoinSafe = pathJoinSafe;
/** Replace variables in a string like: `Hello {{userName}}!`
* @param {String} content
* @param {Object} varz object with key => value === toReplace => replacer
* @param {Object} options
* * valueWhenNotSet => replacer for undefined values. Default: ''
* * regexp => must be 'g' and first capturing group matching the value to replace. Default: /{{\s*([^}]*)\s*}}/g
*/
function miniTemplater(content, varz, options = {}) {
const options2 = {
valueWhenNotSet: '',
regexp: /{{\s*([^}]*)\s*}}/g,
valueWhenContentUndefined: '',
...options,
};
return (0, isset_1.isset)(content) ? content.replace(options2.regexp, (_, $1) => (0, isset_1.isset)(varz[$1]) ? varz[$1] : options2.valueWhenNotSet) : options2.valueWhenContentUndefined;
}
exports.miniTemplater = miniTemplater;
/** Clean output for outside world. All undefined / null / NaN / Infinity values are changed to '-' */
function cln(val, replacerInCaseItIsUndefinNaN = '-') { return ['undefined', undefined, 'indéfini', 'NaN', NaN, Infinity, null].includes(val) ? replacerInCaseItIsUndefinNaN : val; }
exports.cln = cln;
function nbOccurenceInString(baseString, searchedString, allowOverlapping = false) {
if (searchedString.length === 0)
return baseString.length + 1;
let n = 0;
let pos = 0;
const step = allowOverlapping ? 1 : searchedString.length;
// eslint-disable-next-line no-constant-condition
while (true) {
pos = baseString.indexOf(searchedString, pos);
if (pos >= 0) {
++n;
pos += step;
}
else
break;
}
return n;
}
exports.nbOccurenceInString = nbOccurenceInString;
/** typed lower case. Eg: if you pass 'A' | 'B', the resulting type will be 'a' | 'b' */
function lowerCase(string) {
return string?.toLocaleLowerCase();
}
exports.lowerCase = lowerCase;
/** typed upper case. Eg: if you pass 'a' | 'b', the resulting type will be 'A' | 'B' */
function upperCase(string) {
return string?.toLocaleUpperCase();
}
exports.upperCase = upperCase;
/** Parse strings like 'true', 'false', '123', 'null' to their real type equivalent. Actual string is returned if nothing matches.
* * /!\ for typing please profide a type parameter like `parseStringVariable<boolean>('true')` */
function parseStringVariable(val) {
if (val === 'undefined')
return undefined;
else if (val === 'true')
return true;
else if (val === 'false')
return false;
else if (val === 'null')
return null;
else if (/^[0-9]+$/.test(val))
return Number(val);
else
return val;
}
exports.parseStringVariable = parseStringVariable;
/** return val === 'true' || val === true */
function parseStringAsBoolean(val) {
return val === 'true' || val === true;
}
exports.parseStringAsBoolean = parseStringAsBoolean;
/** return Number(val) */
function parseStringAsNumber(val) {
return Number(val);
}
exports.parseStringAsNumber = parseStringAsNumber;
//# sourceMappingURL=string-utils.js.map