Files
utils/src/string.ts
ztimson 361613f507
All checks were successful
Build / Publish Docs (push) Successful in 1m6s
Build / Build NPM Project (push) Successful in 1m10s
Build / Tag Version (push) Successful in 11s
Added decodeHTML
2026-03-29 22:33:21 -04:00

377 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* String of all letters
*/
export const LETTER_LIST = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
/**
* String of all numbers
*/
export const NUMBER_LIST = '0123456789';
/**
* String of all symbols
*/
export const SYMBOL_LIST = '~`!@#$%^&*()_-+={[}]|\\:;"\'<,>.?/';
/**
* String of all letters, numbers & symbols
*/
export const CHAR_LIST = LETTER_LIST + LETTER_LIST.toLowerCase() + NUMBER_LIST + SYMBOL_LIST;
/**
* Converts text to camelCase
*/
export function camelCase(str?: string): string {
if(!str) return '';
const pascal = pascalCase(str);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
/**
* Decode HTML escaped characters
* @param html HTML to clean up
* @returns {any}
*/
export function decodeHtml(html: string) {
return html
.replace(/&nbsp;/g, '\u00A0')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&cent;/g, '¢')
.replace(/&pound;/g, '£')
.replace(/&yen;/g, '¥')
.replace(/&euro;/g, '€')
.replace(/&copy;/g, '©')
.replace(/&reg;/g, '®')
.replace(/&trade;/g, '™')
.replace(/&times;/g, '×')
.replace(/&divide;/g, '÷')
.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
.replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/&amp;/g, '&'); // Always last!
}
/**
* Convert number of bytes into a human-readable size
*
* @param {number} bytes Number of bytes
* @param {number} decimals Decimal places to preserve
* @return {string} Formated size
*/
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
/**
* Convert milliseconds to human-readable duration
* @param {string} ms milliseconds
* @param {boolean} short Use unit initial instead of word
* @return {string} formated duration
*/
export function formatMs(ms: number, short = false): string {
if (isNaN(ms) || ms < 0) return "Invalid input";
const seconds = ms / 1000;
const minutes = seconds / 60;
const hours = minutes / 60;
const days = hours / 24;
if (days >= 1) return `${days.toFixed(1)} ${short ? 'd' : 'days'}`;
else if (hours >= 1) return `${hours.toFixed(1)} ${short ? 'h' : 'hours'}`;
else if (minutes >= 1) return `${minutes.toFixed(1)} ${short ? 'm' : 'minutes'}`;
else return `${seconds.toFixed(1)} ${short ? 's' : 'seconds'}`;
}
/**
* Extract numbers from a string & create a formated phone number: +1 (123) 456-7890
*
* @param {string} number String that will be parsed for numbers
* @return {string} Formated phone number
*/
export function formatPhoneNumber(number: string) {
const parts = /(\+?1)?.*?(\d{3}).*?(\d{3}).*?(\d{4})/g.exec(number);
if(!parts) throw new Error(`Number cannot be parsed: ${number}`);
return `${parts[1] ? '+1' : ''} (${parts[2]}) ${parts[3]}-${parts[4]}`.trim();
}
/**
* Insert a string into another string at a given position
*
* @example
* ```js
* console.log(insertAt('Hello world!', ' glorious', 5);
* // Output: Hello glorious world!
* ```
*
* @param {string} target - Parent string you want to modify
* @param {string} str - Value that will be injected to parent
* @param {number} index - Position to inject string at
* @returns {string} - New string
* @deprecated use `strSplice()`
*/
export function insertAt(target: string, str: string, index: number): String {
return `${target.slice(0, index)}${str}${target.slice(index + 1)}`;
}
/**
* Converts text to kebab-case
*/
export function kebabCase(str?: string): string {
if(!str) return '';
return wordSegments(str).map(w => w.toLowerCase()).join("-");
}
/**
* Add padding to string
*
* @example
* ```js
* const now = new Date();
* const padded = now.getHours() + ':' + pad(now.getMinutes(), 2, '0');
* console.log(padded); // Output: "2:05"
* ```
*
* @param text Text that will be padded
* @param {number} length Target length
* @param {string} char Character to use as padding, defaults to space
* @param {boolean} start Will pad start of text if true, or the end if false
* @return {string} Padded string
* @deprecated Please use `String.padStart` & `String.padEnd`
*/
export function pad(text: any, length: number, char: string = ' ', start = true) {
if(start) return text.toString().padStart(length, char);
return text.toString().padEnd(length, char);
}
/**
* Convert text to PascalCase
* @param {string} str
* @return {string}
*/
export function pascalCase(str?: string): string {
if(!str) return '';
return wordSegments(str)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join('');
}
/**
* Remove all emojis from a string
* @param {string} str Input string with emojis
* @returns {string} Sanitized string without emojis
*/
export function removeEmojis(str: string): string {
const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud83c[\udde6-\uddff]|[\ud83d[\ude00-\ude4f]|[\ud83d[\ude80-\udeff]|[\ud83c[\udd00-\uddff]|[\ud83d[\ude50-\ude7f]|[\u2600-\u26ff]|[\u2700-\u27bf]|[\ud83e[\udd00-\uddff]|[\ud83c[\udf00-\uffff]|[\ud83d[\ude00-\udeff]|[\ud83c[\udde6-\uddff])/g;
return str.replace(emojiRegex, '');
}
/**
* Generate a random hexadecimal value
*
* @param {number} length Number of hexadecimal place values
* @return {string} Hexadecimal number as a string
*/
export function randomHex(length: number) {
return Array(length).fill(null).map(() => Math.round(Math.random() * 0xF).toString(16)).join('');
}
/**
* Generate a string of random characters.
*
* @example
* ```ts
* const random = randomString();
* const randomByte = randomString(8, "01")
* ```
*
* @param {number} length - length of generated string
* @param {string} pool - character pool to generate string from
* @return {string} generated string
*/
export function randomString(length: number, pool: string = CHAR_LIST): string {
return Array(length).fill(null).map(() => {
const n = ~~(Math.random() * pool.length);
return pool[n];
}).join('');
}
/**
* Generate a random string with fine control over letters, numbers & symbols
*
* @example
* ```ts
* const randomLetter = randomString(1, true);
* const randomChar = randomString(1, true, true, true);
* ```
*
* @param {number} length - length of generated string
* @param {boolean} letters - Add letters to pool
* @param {boolean} numbers - Add numbers to pool
* @param {boolean} symbols - Add symbols to pool
* @return {string} generated string
*/
export function randomStringBuilder(length: number, letters = false, numbers = false, symbols = false): string {
if(!letters && !numbers && !symbols) throw new Error('Must enable at least one: letters, numbers, symbols');
return Array(length).fill(null).map(() => {
let c;
do {
const type = ~~(Math.random() * 3);
if(letters && type == 0) {
c = LETTER_LIST[~~(Math.random() * LETTER_LIST.length)];
} else if(numbers && type == 1) {
c = NUMBER_LIST[~~(Math.random() * NUMBER_LIST.length)];
} else if(symbols && type == 2) {
c = SYMBOL_LIST[~~(Math.random() * SYMBOL_LIST.length)];
}
} while(!c);
return c;
}).join('');
}
/**
* Converts text to snake_case
*/
export function snakeCase(str?: string): string {
if(!str) return '';
return wordSegments(str).map(w => w.toLowerCase()).join("_");
}
/**
* Splice a string together (Similar to Array.splice)
*
* @param {string} str String that will be modified
* @param {number} start Start index of splice
* @param {number} deleteCount Number of characters to remove in splice
* @param {string} insert Insert new string into splice
* @return {string} Returned spliced string
*/
export function strSplice(str: string, start: number, deleteCount: number, insert = '') {
const before = str.slice(0, start);
const after = str.slice(start + deleteCount);
return before + insert + after;
}
/**
* Find all substrings that match a given pattern.
*
* Roughly based on `String.prototype.matchAll`.
*
* @param {string} value - String to search.
* @param {RegExp | string} regex - Regular expression to match.
* @return {RegExpExecArray[]} Found matches.
*/
export function matchAll(value: string, regex: RegExp | string): RegExpExecArray[] {
if(typeof regex === 'string') {
regex = new RegExp(regex, 'g');
}
// https://stackoverflow.com/a/60290199
if(!regex.global) {
throw new TypeError('Regular expression must be global.');
}
let ret: RegExpExecArray[] = [];
let match: RegExpExecArray | null;
while((match = regex.exec(value)) !== null) {
ret.push(match);
}
return ret;
}
/** Parts of a URL */
export type ParsedUrl = {
protocol?: string,
subdomain?: string,
domain: string,
host: string,
port?: number,
path?: string,
query?: {[name: string]: string}
fragment?: string
}
/**
* Break a URL string into its parts for easy parsing
*
* @param {string} url URL string that will be parsed
* @returns {RegExpExecArray} Parts of URL
* @deprecated Use built-in URL object: `new URL('...')`;
*/
export function parseUrl(url: string): ParsedUrl {
const processed = new RegExp(
'(?:(?<protocol>[\\w\\d]+)\\:\\/\\/)?(?:(?<user>.+)\\@)?(?<host>(?<domain>[^:\\/\\?#@\\n]+)(?:\\:(?<port>\\d*))?)(?<path>\\/.*?)?(?:\\?(?<query>.*?))?(?:#(?<fragment>.*?))?$',
'gm').exec(url);
const groups: ParsedUrl = <any>processed?.groups ?? {};
const domains = groups.domain.split('.');
if(groups['port'] != null) groups.port = Number(groups.port);
if(domains.length > 2) {
groups.domain = domains.splice(-2, 2).join('.');
groups.subdomain = domains.join('.');
}
if(groups.query) {
const split = (<any>groups.query).split('&'), query: any = {};
split.forEach((q: any) => {
const [key, val] = q.split('=');
query[key] = val;
});
groups.query = query;
}
return groups;
}
/**
* Create MD5 hash using native javascript
*
* @param d String to hash
* @returns {string} Hashed string
*/
export function md5(d: string) {
var r = M(V(Y(X(d),8*d.length)));return r.toLowerCase()};
function M(d:any){for(var _,m="0123456789ABCDEF",f="",r=0;r<d.length;r++)_=d.charCodeAt(r),f+=m.charAt(_>>>4&15)+m.charAt(15&_);return f}
function X(d:any){for(var _=Array(d.length>>2),m=0;m<_.length;m++)_[m]=0;for(m=0;m<8*d.length;m+=8)_[m>>5]|=(255&d.charCodeAt(m/8))<<m%32;return _}
function V(d:any){for(var _="",m=0;m<32*d.length;m+=8)_+=String.fromCharCode(d[m>>5]>>>m%32&255);return _}
function Y(d:any,_:any){d[_>>5]|=128<<_%32,d[14+(_+64>>>9<<4)]=_;for(var m=1732584193,f=-271733879,r=-1732584194,i=271733878,n=0;n<d.length;n+=16){var h=m,t=f,g=r,e=i;f=md5_ii(f=md5_ii(f=md5_ii(f=md5_ii(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_ff(f=md5_ff(f=md5_ff(f=md5_ff(f,r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+0],7,-680876936),f,r,d[n+1],12,-389564586),m,f,d[n+2],17,606105819),i,m,d[n+3],22,-1044525330),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+4],7,-176418897),f,r,d[n+5],12,1200080426),m,f,d[n+6],17,-1473231341),i,m,d[n+7],22,-45705983),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+8],7,1770035416),f,r,d[n+9],12,-1958414417),m,f,d[n+10],17,-42063),i,m,d[n+11],22,-1990404162),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+12],7,1804603682),f,r,d[n+13],12,-40341101),m,f,d[n+14],17,-1502002290),i,m,d[n+15],22,1236535329),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+1],5,-165796510),f,r,d[n+6],9,-1069501632),m,f,d[n+11],14,643717713),i,m,d[n+0],20,-373897302),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+5],5,-701558691),f,r,d[n+10],9,38016083),m,f,d[n+15],14,-660478335),i,m,d[n+4],20,-405537848),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+9],5,568446438),f,r,d[n+14],9,-1019803690),m,f,d[n+3],14,-187363961),i,m,d[n+8],20,1163531501),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+13],5,-1444681467),f,r,d[n+2],9,-51403784),m,f,d[n+7],14,1735328473),i,m,d[n+12],20,-1926607734),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+5],4,-378558),f,r,d[n+8],11,-2022574463),m,f,d[n+11],16,1839030562),i,m,d[n+14],23,-35309556),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+1],4,-1530992060),f,r,d[n+4],11,1272893353),m,f,d[n+7],16,-155497632),i,m,d[n+10],23,-1094730640),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+13],4,681279174),f,r,d[n+0],11,-358537222),m,f,d[n+3],16,-722521979),i,m,d[n+6],23,76029189),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+9],4,-640364487),f,r,d[n+12],11,-421815835),m,f,d[n+15],16,530742520),i,m,d[n+2],23,-995338651),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+0],6,-198630844),f,r,d[n+7],10,1126891415),m,f,d[n+14],15,-1416354905),i,m,d[n+5],21,-57434055),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+12],6,1700485571),f,r,d[n+3],10,-1894986606),m,f,d[n+10],15,-1051523),i,m,d[n+1],21,-2054922799),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+8],6,1873313359),f,r,d[n+15],10,-30611744),m,f,d[n+6],15,-1560198380),i,m,d[n+13],21,1309151649),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+4],6,-145523070),f,r,d[n+11],10,-1120210379),m,f,d[n+2],15,718787259),i,m,d[n+9],21,-343485551),m=safe_add(m,h),f=safe_add(f,t),r=safe_add(r,g),i=safe_add(i,e)}return Array(m,f,r,i)}
function md5_cmn(d:any,_:any,m:any,f:any,r:any,i:any){return safe_add(bit_rol(safe_add(safe_add(_,d),safe_add(f,i)),r),m)}
function md5_ff(d:any,_:any,m:any,f:any,r:any,i:any,n:any){return md5_cmn(_&m|~_&f,d,_,r,i,n)}
function md5_gg(d:any,_:any,m:any,f:any,r:any,i:any,n:any){return md5_cmn(_&f|m&~f,d,_,r,i,n)}
function md5_hh(d:any,_:any,m:any,f:any,r:any,i:any,n:any){return md5_cmn(_^m^f,d,_,r,i,n)}
function md5_ii(d:any,_:any,m:any,f:any,r:any,i:any,n:any){return md5_cmn(m^(_|~f),d,_,r,i,n)}
function safe_add(d:any,_:any){var m=(65535&d)+(65535&_);return(d>>16)+(_>>16)+(m>>16)<<16|65535&m}
function bit_rol(d:any,_:any){return d<<_|d>>>32-_
}
/**
* Splits a string into logical word segments
*/
export function wordSegments(str?: string): string[] {
if (!str) return [];
return str
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.replace(/([0-9]+)([a-zA-Z])/g, "$1 $2")
.replace(/([a-zA-Z])([0-9]+)/g, "$1 $2")
.replace(/[_\-\s]+/g, " ")
.trim()
.split(/\s+/)
.filter(Boolean);
}
/**
* Check if email is valid
*
* @param {string} email - Target
* @returns {boolean} - Follows format
*/
export function validateEmail(email: string) {
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(email);
}