aurelia-path
Version:
Utilities for path manipulation.
284 lines (258 loc) • 8.92 kB
text/typescript
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.');
}
}