UNPKG

aurelia-path

Version:

Utilities for path manipulation.

284 lines (258 loc) 8.92 kB
function trimDots(ary: string[]): void { for (let i = 0; i < ary.length; ++i) { let part = ary[i]; if (part === '.') { ary.splice(i, 1); i -= 1; } else if (part === '..') { // If at the start, or previous value is still .., // keep them so that when converted to a path it may // still work when converted to a path, even though // as an ID it is less than ideal. In larger point // releases, may be better to just kick out an error. if (i === 0 || (i === 1 && ary[2] === '..') || ary[i - 1] === '..') { continue; } else if (i > 0) { ary.splice(i - 1, 2); i -= 2; } } } } /** * Calculates a path relative to a file. * * @param name The relative path. * @param file The file path. * @return The calculated path. */ export function relativeToFile(name: string, file: string): string { let fileParts = file && file.split('/'); let nameParts = name.trim().split('/'); if (nameParts[0].charAt(0) === '.' && fileParts) { //Convert file to array, and lop off the last part, //so that . matches that 'directory' and not name of the file's //module. For instance, file of 'one/two/three', maps to //'one/two/three.js', but we want the directory, 'one/two' for //this normalization. let normalizedBaseParts = fileParts.slice(0, fileParts.length - 1); nameParts.unshift(...normalizedBaseParts); } trimDots(nameParts); return nameParts.join('/'); } /** * Joins two paths. * * @param path1 The first path. * @param path2 The second path. * @return The joined path. */ export function join(path1: string, path2: string): string { if (!path1) { return path2; } if (!path2) { return path1; } let schemeMatch = path1.match(/^([^/]*?:)\//); let scheme = (schemeMatch && schemeMatch.length > 0) ? schemeMatch[1] : ''; path1 = path1.substr(scheme.length); let urlPrefix; if (path1.indexOf('///') === 0 && scheme === 'file:') { urlPrefix = '///'; } else if (path1.indexOf('//') === 0) { urlPrefix = '//'; } else if (path1.indexOf('/') === 0) { urlPrefix = '/'; } else { urlPrefix = ''; } let trailingSlash = path2.slice(-1) === '/' ? '/' : ''; let url1 = path1.split('/'); let url2 = path2.split('/'); let url3 = []; for (let i = 0, ii = url1.length; i < ii; ++i) { if (url1[i] === '..') { // retain leading .. // don't pop out previous .. // retain consecutive ../../.. if (url3.length && url3[url3.length - 1] !== '..') { url3.pop(); } else { url3.push(url1[i]); } } else if (url1[i] === '.' || url1[i] === '') { continue; } else { url3.push(url1[i]); } } for (let i = 0, ii = url2.length; i < ii; ++i) { if (url2[i] === '..') { if (url3.length && url3[url3.length - 1] !== '..') { url3.pop(); } else { url3.push(url2[i]); } } else if (url2[i] === '.' || url2[i] === '') { continue; } else { url3.push(url2[i]); } } return scheme + urlPrefix + url3.join('/') + trailingSlash; } let encode = encodeURIComponent; const dollarSignRegex: RegExp = /%24/g; let encodeKey = k => encode(k).replace(dollarSignRegex, '$'); /** * Recursively builds part of query string for parameter. * * @param key Parameter name for query string. * @param value Parameter value to deserialize. * @param traditional Boolean Use the old URI template standard (RFC6570) * @return Array with serialized parameter(s) */ function buildParam(key: string, value: any, traditional?: boolean): Array<string> { let result = []; if (value === null || value === undefined) { return result; } if (Array.isArray(value)) { for (let i = 0, l = value.length; i < l; i++) { if (traditional) { result.push(`${encodeKey(key)}=${encode(value[i])}`); } else { let arrayKey = key + '[' + (typeof value[i] === 'object' && value[i] !== null ? i : '') + ']'; result = result.concat(buildParam(arrayKey, value[i])); } } } else if (typeof (value) === 'object' && !traditional) { for (let propertyName in value) { result = result.concat(buildParam(key + '[' + propertyName + ']', value[propertyName])); } } else { result.push(`${encodeKey(key) }=${encode(value) }`); } return result; } /** * Generate a query string from an object. * * @param params Object containing the keys and values to be used. * @param traditional Boolean Use the old URI template standard (RFC6570) * @returns The generated query string, excluding leading '?'. */ export function buildQueryString(params?: Object, traditional?: boolean): string { let pairs = []; let keys = Object.keys(params || {}).sort(); for (let i = 0, len = keys.length; i < len; i++) { let key = keys[i]; pairs = pairs.concat(buildParam(key, params[key], traditional)); } if (pairs.length === 0) { return ''; } return pairs.join('&'); } /** * Process parameter that was recognized as scalar param (primitive value or shallow array). * * @param existedParam Object with previously parsed values for specified key. * @param value Parameter value to append. * @returns Initial primitive value or transformed existedParam if parameter was recognized as an array. */ function processScalarParam(existedParam: Object, value: Object): Object { if (Array.isArray(existedParam)) { // value is already an array, so push on the next value. existedParam.push(value); return existedParam; } if (existedParam !== undefined) { // value isn't an array, but since a second value has been specified, // convert value into an array. return [existedParam, value]; } // value is a scalar. return value; } /** * Sequentially process parameter that was recognized as complex value (object or array). * For each keys part, if the current level is undefined create an * object or array based on the type of the next keys part. * * @param queryParams root-level result object. * @param keys Collection of keys related to this parameter. * @param value Parameter value to append. */ function parseComplexParam(queryParams: Object, keys: (string | number)[], value: any): void { let currentParams = queryParams; let keysLastIndex = keys.length - 1; for (let j = 0; j <= keysLastIndex; j++) { let key = keys[j] === '' ? (currentParams as any).length : keys[j]; preventPollution(key); if (j < keysLastIndex) { // The value has to be an array or a false value // It can happen that the value is no array if the key was repeated with traditional style like `list=1&list[]=2` let prevValue = !currentParams[key] || typeof currentParams[key] === 'object' ? currentParams[key] : [currentParams[key]]; currentParams = currentParams[key] = prevValue || (isNaN(keys[j + 1] as number) ? {} : []); } else { currentParams = currentParams[key] = value; } } } /** * Parse a query string. * * @param queryString The query string to parse. * @returns Object with keys and values mapped from the query string. */ export function parseQueryString(queryString: string): Object { let queryParams = {}; if (!queryString || typeof queryString !== 'string') { return queryParams; } let query = queryString; if (query.charAt(0) === '?') { query = query.substr(1); } let pairs = query.replace(/\+/g, ' ').split('&'); for (let i = 0; i < pairs.length; i++) { let pair = pairs[i].split('='); let key = decodeURIComponent(pair[0]); if (!key) { continue; } //split object key into its parts let keys = key.split(']['); let keysLastIndex = keys.length - 1; // If the first keys part contains [ and the last ends with ], then [] // are correctly balanced, split key to parts //Else it's basic key if (/\[/.test(keys[0]) && /\]$/.test(keys[keysLastIndex])) { keys[keysLastIndex] = keys[keysLastIndex].replace(/\]$/, ''); keys = keys.shift().split('[').concat(keys); keysLastIndex = keys.length - 1; } else { keysLastIndex = 0; } if (pair.length >= 2) { let value = pair[1] ? decodeURIComponent(pair[1]) : ''; if (keysLastIndex) { parseComplexParam(queryParams, keys, value); } else { preventPollution(key); queryParams[key] = processScalarParam(queryParams[key], value); } } else { queryParams[key] = true; } } return queryParams; } function preventPollution(key: string) { if (key === '__proto__') { throw new Error('Prototype pollution detected.'); } }