ConceptPortal-public/rsconcept/frontend/src/models/FolderTree.ts
Ivan 50760d7d13
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
R: Upgrade to eslint9
2024-08-06 14:39:00 +03:00

160 lines
3.6 KiB
TypeScript

/**
* Module: Folder tree data structure. Does not support deletions.
*/
/**
* Represents single node of a {@link FolderTree}.
*/
export class FolderNode {
rank: number = 0;
text: string;
children: Map<string, FolderNode>;
parent: FolderNode | undefined;
filesInside: number = 0;
filesTotal: number = 0;
constructor(text: string, parent?: FolderNode) {
this.text = text;
this.parent = parent;
this.children = new Map();
if (parent) {
this.rank = parent.rank + 1;
}
}
addChild(text: string): FolderNode {
const node = new FolderNode(text, this);
this.children.set(text, node);
return node;
}
hasPredecessor(target: FolderNode): boolean {
if (this.parent === target) {
return true;
} else if (!this.parent) {
return false;
}
let node = this.parent;
while (node.parent) {
if (node.parent === target) {
return true;
}
node = node.parent;
}
return false;
}
incrementFiles(count: number = 1): void {
this.filesInside = this.filesInside + count;
this.incrementTotal(count);
}
incrementTotal(count: number = 1): void {
this.filesTotal = this.filesTotal + count;
if (this.parent) {
this.parent.incrementTotal(count);
}
}
getPath(): string {
const suffix = this.text ? `/${this.text}` : '';
if (!this.parent) {
return suffix;
} else {
return this.parent.getPath() + suffix;
}
}
}
/**
* Represents a FolderTree.
*
*/
export class FolderTree {
roots = new Map<string, FolderNode>();
constructor(arr?: string[]) {
arr?.forEach(path => this.addPath(path));
}
at(path: string): FolderNode | undefined {
let parse = ChopPathHead(path);
if (!this.roots.has(parse.head)) {
return undefined;
}
let node = this.roots.get(parse.head)!;
while (parse.tail !== '') {
parse = ChopPathHead(parse.tail);
if (!node.children.has(parse.head)) {
return undefined;
}
node = node.children.get(parse.head)!;
}
return node;
}
getTree(): FolderNode[] {
const result: FolderNode[] = [];
this.roots.forEach(root => this.visitNode(root, result));
return result;
}
private visitNode(target: FolderNode, result: FolderNode[]) {
result.push(target);
[...target.children.keys()]
.sort((a, b) => a.localeCompare(b))
.forEach(key => this.visitNode(target.children.get(key)!, result));
}
addPath(path: string, filesCount: number = 1): FolderNode {
let parse = ChopPathHead(path);
if (!parse.head) {
throw Error(`Invalid path ${path}`);
}
let node = this.roots.has(parse.head) ? this.roots.get(parse.head)! : this.addNode(parse.head);
while (parse.tail !== '') {
parse = ChopPathHead(parse.tail);
if (node.children.has(parse.head)) {
node = node.children.get(parse.head)!;
} else {
node = this.addNode(parse.head, node);
}
}
node.incrementFiles(filesCount);
return node;
}
private addNode(text: string, parent?: FolderNode): FolderNode {
if (parent === undefined) {
const newNode = new FolderNode(text);
this.roots.set(text, newNode);
return newNode;
} else {
return parent.addChild(text);
}
}
}
// ========= Internals =======
function ChopPathHead(path: string) {
if (!path || path.at(0) !== '/') {
return {
head: '',
tail: ''
};
}
const slash = path.indexOf('/', 1);
if (slash === -1) {
return {
head: path.substring(1),
tail: ''
};
} else {
return {
head: path.substring(1, slash),
tail: path.substring(slash)
};
}
}