export function upcaseFirstChar(str: string) { return str.charAt(0).toUpperCase() + str.slice(1) } export function toTitleCase(str: string) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => { return letter.toUpperCase(); }); } export function toSnakeCase(str: string) { return str.split(/(?=[A-Z])/).join('_').toLowerCase(); } export function toCamelCase(str: string) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => { return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); }).replace(/\s+/g, ''); } export function toClassCase(str: string) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => { return letter.toUpperCase(); }).replace(/\s+/g, ''); } export function toPascalCase(str: string) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase()).replace(/\s+/g, ''); } function validateUrlBit(bit) { if (typeof bit !== 'string') { throw new TypeError('Url must be a string. Received ' + bit); } } const URL_PATH_SPLIT = /\/+/g; function url_join(strArray: string[], { resolve = true, trailing_slash = null }: { resolve?: boolean, trailing_slash?: boolean | null, } = {}) { if (strArray.length === 0) { return ''; } validateUrlBit(strArray[0]) // If the first part is a plain protocol, we combine it with the next part. if (strArray[0].match(/^[^/:]+:\/*$/) && strArray.length > 1) { const first = strArray.shift(); strArray[0] = first + strArray[0]; } // There must be two or three slashes in the file protocol, two slashes in anything else. if (strArray[0].match(/^file:\/\/\//)) { strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1:///'); } else { strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1://'); } let resultArray: string[] = []; for (let i = 0; i < strArray.length; i++) { let component = strArray[i]; if (component == null) continue; component = String(component); validateUrlBit(component); const hasProtocol = component.match(/^([^\/:]+):\/*/); // This bit's absolute if (resolve) { if (component[0] == '/') { if (resultArray[0] && resultArray[0][0] != '/') { const match = /^(\w+:\/\/)?(.*?)(\/.*)$/.exec(resultArray[0]); const domain = (match[1] || '') + (match[2] || ''); component = domain + component; } resultArray = [] } else if (hasProtocol) { resultArray = [] } } else if (hasProtocol) { throw new Error("Cannot join multiple complete URLs unless resolve is true") } resultArray.push(component); } // Each input component is now separated by a single slash except the possible first plain protocol part. let str = resultArray.join('/'); let protocol = ""; const pidx = str.indexOf('://'); if (pidx >= 0) { protocol = str.substring(0, pidx + 3); str = str.substring(pidx + 3); } if (resolve) { const resolvedBits = []; for (let bit of str.split(URL_PATH_SPLIT)) { if (bit == '.') continue; else if (bit == '..') resolvedBits.pop(); else resolvedBits.push(bit); } str = resolvedBits.join('/') } else { // Clean multiple slashes str = str.replace(URL_PATH_SPLIT, '/'); } str = protocol + str; // remove trailing slash before parameters or hash if (!trailing_slash) str = str.replace(/\/(\?|&|#[^!])/g, '$1'); // replace ? in parameters with & const split_params = str.split('?'); let url = split_params.shift(); if (trailing_slash && !url.endsWith('/')) url = url + '/'; if (trailing_slash === false) url = url.replace(/\/+$/, ''); str = url + (split_params.length > 0 ? '?' : '') + split_params.join('&'); return str; } type RouterMatch = { url: string, path: string, [key: string]: any } /** * (Intelligently) Joins multiple parts of a URL. * * Accepts strings or ReactRouter `Matches` as URL bits. */ export function urlJoin(parts: (RouterMatch | string)[], options?: Parameters[1]) export function urlJoin(...parts: (RouterMatch | string)[]) export function urlJoin() { if (Array.isArray(arguments[0])) { const parts = arguments[0] const bits = parts.map((b) => { if (typeof b == 'object') b = b.path || b.url; return b; }) return url_join(bits, arguments[1]); } else { return urlJoin(Array.prototype.slice.call(arguments)); } } /** * Template string tag to (UN-intelligently) join multiple parts of a URL. * This tag does not attempt to resolve absolute vs relative paths, but instead strips duplicate `/` characters * if, for example, `http://domain.com/` and `/blah` were joined (resulting in `http://domain.com/blah`). * * Accepts strings or ReactRouter `Matches` as URL bits. */ export const url = (literals: TemplateStringsArray, ...placeholders: (RouterMatch | string)[]) => { let result = ""; // interleave the literals with the placeholders for (let i = 0; i < placeholders.length; i++) { result += literals[i]; let ph = placeholders[i]; if (!(typeof ph == 'string')) ph = ph.url; if (ph.endsWith('/')) ph = ph.slice(0, -1); result += ph; } // add the last literal result += literals[literals.length - 1]; // Resolve . and .. segments const result_bits = result.split('/'); const final_bits = []; for (let bit of result_bits) { if (bit == '..') { final_bits.pop(); } else if (bit == '.') { continue; } else { final_bits.push(bit); } } return final_bits.join('/'); } /** * Template string tag to split a string into a list of words. Similar to Ruby's `%w[]` * * ```js * words`hello large world` -> ['hello', 'large', 'world'] * ``` */ export const words = (literals: TemplateStringsArray, ...placeholders: string[]) => { let result = ""; // interleave the literals with the placeholders for (let i = 0; i < placeholders.length; i++) { result += literals[i] + placeholders[i]; } // add the last literal result += literals[literals.length - 1]; return result.split(' '); }