mono/packages/vfs/ref/model/Path.ts

330 lines
8.3 KiB
TypeScript

import { clone } from '@xblox/core/objects';
import { isString } from '@xblox/core/primitives';
export const slash = (str: string) => {
let isExtendedLengthPath = /^\\\\\?\\/.test(str);
let hasNonAscii = /[^\x00-\x80]+/.test(str);
if (isExtendedLengthPath || hasNonAscii) {
return str;
}
return str.replace(/\\/g, '/');
};
export class Path {
path: string;
segments: string[];
hasLeading: boolean;
hasTrailing: boolean;
extension: string;
_parentPath: Path;
static EMPTY: Path = new Path('');
static normalize(path: string): string {
return slash(path).replace(/\/+/g, '\/');
}
constructor(path: string | string[] = '.', hasLeading: boolean = false, hasTrailing: boolean = false) {
if (isString(path)) {
this.path = path;
this.getSegments();
} else {
this.segments = path;
this.hasLeading = hasLeading;
this.hasTrailing = hasTrailing;
}
}
endsWith(tail): boolean {
let segments = clone(this.segments);
let tailSegments = (new Path(tail)).getSegments();
while (tailSegments.length > 0 && segments.length > 0) {
if (tailSegments.pop() !== segments.pop()) {
return false;
}
}
return true;
}
getExtension(): string {
if (!this.extension) {
this.extension = this.path.substr(this.path.lastIndexOf('.') + 1);
}
return this.extension;
}
segment(index): string {
const segs = this.getSegments();
if (segs.length < index) {
return null;
}
return segs[index];
}
/**
* Return all items under this path
* @param items {String[]}
* @param recursive {boolean}
* @returns {String[]}
*/
getChildren(items: string[], recursive: boolean = false) {
let result = [];
let root = this,
path = this.toString();
const addChild = (child) => {
let _path = typeof child !== 'string' ? child.toString() : child;
if (_path !== path && result.indexOf(_path) === -1) {
result.push(_path);
}
};
items.forEach((item) => {
let child = new Path(item);
// root match
if (child.startsWith(root)) {
if (recursive) {
addChild(child.toString());
} else {
let diff = child.relativeTo(path);
if (diff) {
let diffSegments = diff.getSegments();
// direct child
if (diffSegments.length === 1) {
addChild(child);
} else if (diffSegments.length > 1) {
// make sure that its parent has been added:
let parent = child.getParentPath();
let parentDiff = parent.relativeTo(path);
// check diff again
if (parentDiff.getSegments().length === 1) {
addChild(parent.toString());
}
}
}
}
}
});
return result;
}
getSegments(): string[] {
if (!this.segments) {
let path = this.path;
this.segments = path.split('/');
if (path.charAt(0) === '/') {
this.hasLeading = true;
}
if (path.charAt(path.length - 1) === '/') {
this.hasTrailing = true;
// If the path ends in '/', split() will create an array whose last element
// is an empty string. Remove that here.
this.segments.pop();
}
this._canonicalize();
}
return this.segments;
}
isAbsolute(): boolean {
return this.hasLeading;
}
getParentPath(): Path {
if (!this._parentPath) {
let parentSegments = clone(this.segments);
parentSegments.pop();
this._parentPath = new Path(parentSegments, this.hasLeading);
}
return this._parentPath;
}
_clone(): Path {
return new Path(clone(this.segments), this.hasLeading, this.hasTrailing);
}
append(tail: string | Path): Path {
tail = tail || '';
if (typeof tail === 'string') {
tail = new Path(tail);
}
if (tail.isAbsolute()) {
return tail;
}
let mySegments = this.segments;
let tailSegments = tail.getSegments();
let newSegments = mySegments.concat(tailSegments);
let result = new Path(newSegments, this.hasLeading, tail.hasTrailing);
if (tailSegments[0] === '..' || tailSegments[0] === '.') {
result._canonicalize();
}
return result;
}
toString(): string {
let result = [];
if (this.hasLeading) {
result.push('/');
}
for (let i = 0; i < this.segments.length; i++) {
if (i > 0) {
result.push('/');
}
result.push(this.segments[i]);
}
if (this.hasTrailing) {
result.push('/');
}
return result.join('').replace(/\/+/g, '\/');
}
_toString(): string {
let result = [];
if (this.hasLeading) {
result.push('/');
}
for (let i = 0; i < this.segments.length; i++) {
if (i > 0) {
result.push('/');
}
result.push(this.segments[i]);
}
if (this.hasTrailing) {
result.push('/');
}
return result.join('');
}
removeRelative(): Path {
let segs = this.getSegments();
if (segs.length > 0 && segs[1] === '.') {
return this.removeFirstSegments(1);
}
return this;
}
relativeTo(base: string | Path, ignoreFilename: boolean = false) {
if (typeof base === 'string') {
base = new Path(base);
}
let mySegments = this.segments;
if (this.isAbsolute()) {
return this;
}
let baseSegments = base.getSegments();
let commonLength = this.matchingFirstSegments(base);
let baseSegmentLength = baseSegments.length;
if (ignoreFilename) {
baseSegmentLength = baseSegmentLength - 1;
}
let differenceLength = baseSegmentLength - commonLength;
let newSegmentLength = differenceLength + mySegments.length - commonLength;
if (newSegmentLength === 0) {
return Path.EMPTY;
}
let newSegments = [];
for (let i = 0; i < differenceLength; i++) {
newSegments.push('..');
}
for (let i = commonLength; i < mySegments.length; i++) {
newSegments.push(mySegments[i]);
}
return new Path(newSegments, false, this.hasTrailing);
}
startsWith(anotherPath: Path) {
let count = this.matchingFirstSegments(anotherPath as Path);
return anotherPath._length() === count;
}
_length(): number {
return this.segments.length;
}
matchingFirstSegments(anotherPath: Path) {
let mySegments = this.segments;
let pathSegments = anotherPath.getSegments();
let max = Math.min(mySegments.length, pathSegments.length);
let count = 0;
for (let i = 0; i < max; i++) {
if (mySegments[i] !== pathSegments[i]) {
return count;
}
count++;
}
return count;
}
removeFirstSegments(count: number): Path {
return new Path(this.segments.slice(count, this.segments.length), this.hasLeading, this.hasTrailing);
}
removeMatchingLastSegments(anotherPath: Path): Path {
let match = this.matchingFirstSegments(anotherPath);
return this.removeLastSegments(match);
}
removeMatchingFirstSegments(anotherPath: Path): Path {
let match = this.matchingFirstSegments(anotherPath);
return this._clone().removeFirstSegments(match);
}
removeLastSegments(count: number): Path {
if (!count) {
count = 1;
}
return new Path(this.segments.slice(0, this.segments.length - count), this.hasLeading, this.hasTrailing);
}
lastSegment(): string {
return this.segments[this.segments.length - 1];
}
firstSegment(length: number): string {
return this.segments[length || 0];
}
equals(anotherPath): boolean {
if (this.segments.length !== anotherPath.segments.length) {
return false;
}
for (let i = 0; i < this.segments.length; i++) {
if (anotherPath.segments[i] !== this.segments[i]) {
return false;
}
}
return true;
}
_canonicalize(): void {
let doIt;
let segments = this.segments;
for (let i = 0; i < segments.length; i++) {
if (segments[i] === '.' || segments[i] === '..') {
doIt = true;
break;
}
}
if (doIt) {
let stack = [];
for (let i = 0; i < segments.length; i++) {
if (segments[i] === '..') {
if (stack.length === 0) {
// if the stack is empty we are going out of our scope
// so we need to accumulate segments. But only if the original
// path is relative. If it is absolute then we can't go any higher than
// root so simply toss the .. references.
if (!this.hasLeading) {
stack.push(segments[i]); // stack push
}
} else {
// if the top is '..' then we are accumulating segments so don't pop
if ('..' === stack[stack.length - 1]) {
stack.push('..');
} else {
stack.pop();
}
}
// collapse current references
} else if (segments[i] !== '.' || this.segments.length === 1) {
stack.push(segments[i]); // stack push
}
}
// if the number of segments hasn't changed, then no modification needed
if (stack.length === segments.length) {
return;
}
this.segments = stack;
}
}
}