/**
* The class of the TreeWithPath tree
*/
class Tree
{
/**
* Creates a tree
*
* @param data Tree root node data. The name of the root node is always root
* @constructor
* @example
* const Tree = require("treewithpath");
* const tree = new Tree({ text: "Hello, world!", "otherText": "hoI!" });
*/
constructor(data) {
this._root = new Node("root", data, this);
}
/**
* Root node of this tree
*
* @returns {Node} Root node
* @this {Tree}
*/
get root() {
return this._root;
}
/**
* Adds a node to the tree and returns it
*
* @param {string} name The name of the node to add
* @param data The data of the node to be created
* @param {string} path The path to the parent of the node to create
* @this {Tree} Tree
* @return {Node} Created node
* @throws {TreeError} In case the node already exists
* @example
* tree.add("node2", { text: "Hello, world!", "otherText": "hoI!" }, "/node1");
*/
add(name, data, path) {
if (this.has(Tree.joinPath(path, name))) {
throw new TreeError("This node already exists");
}
const node = new Node(name, data, this);
this.get(path)._children.push(node);
return node;
}
/**
* Gets the node at the specified path
*
* @param {string} path The path to the node to receive
* @param {boolean} error Optional parameter. The default is true. If true, an exception will be thrown if the path is incorrect. Otherwise, null will be returned
* @this {Tree} Tree
* @returns {Node | null} The resulting node or null if error = false and node not found
* @throws {TreeError} In case the node is not found and error = true
* @example
* tree.get("/node1");
*/
get(path, error = true) {
let current = [this.root];
const parsedPath = parsePath(path);
for (const [ index, node ] of parsedPath.entries()) {
const currentNode = current.find(currentNode => currentNode.name === node);
if (currentNode !== undefined) {
current = currentNode._children;
if (index === parsedPath.length - 1) {
return currentNode;
}
} else if (error) {
throw new TreeError(`${node}: Node not exists`);
} else {
return null;
}
}
}
/**
* Deletes the node and returns it at the specified path
*
* @param {string} path The path to the node to be deleted
* @this {Tree} Tree
* @returns {Node} A deleted node that no longer contains children. Children are permanently deleted
* @throws {TreeError} In case the node is not found
* @example
* tree.remove("/node1");
*/
remove(path) {
const node = this.get(path);
node.remove();
return node;
}
/**
* Calls a callback for each node in the tree
*
* @param {Function} callback A function called for each node of the tree. The node in the first argument is passed to the function
* @this {Tree} Tree
* @example
* tree.traverse(node => {
* console.log(node.name);
* });
*/
traverse(callback) {
this.root.traverse(callback);
}
/**
* Returns a tree object suitable for storage in JSON format. This method is mainly used by the JSON.stringify function
*
* @this {Tree} Tree
* @returns {object} A tree object suitable for storage in JSON format
* @example
* tree.toJSON(); // { name: "root", data: { text: "Hello, world!", "otherText": "hoI!" }, children: [{ name: "node1", data: { text: "Hello, world!", "otherText": "hoI!" }, children: [{ name: "node2", data: {text: "Hello, world!", "otherText": "hoI!" }, children: [] }] }
*/
toJSON() {
return this.root.toJSON();
}
/**
* Checks a node for existence in a tree
*
* @param {string} path The path to the node to check
* @returns {boolean} True if the node exists and false if it does not exist
* @this {Tree} Tree
* @example
* tree.has("/notExists/child") // false
* tree.has("/exists") // true
*/
has(path) {
return this.get(path, false) !== null;
}
/**
* Creates a tree from an object that returns the toJSON() method
*
* @param {object} json A tree object suitable for storage in JSON format
* @returns {Tree} Created tree
* @example
* const tree = Tree.fromJSON({ name: "root", data: { text: "Hello, world!", "otherText": "hoI!" }, children: [{name: "node1", data: {text: "Hello, world!", "otherText": "hoI!"}, children: [{name: "node2", data: {text: "Hello, world!", "otherText": "hoI!"}, children: [] }] });
*/
static fromJSON(json) {
const tree = new Tree(json.data);
function recurse(recurseJson, addTo) {
for (const node of recurseJson) {
recurse(node.children, addTo.addChild(node.name, node.data));
}
};
recurse(json.children, tree.root);
return tree;
}
/**
* Connects the two specified paths into one.
*
* @param {string} firstPath First path
* @param {sting} secondPath Second path
* @returns {string} United path
* @example
* Tree.joinPath("/node1", "node2") // /node1/node2
*/
static joinPath(firstPath, secondPath) {
if (firstPath.endsWith("/") && secondPath.startsWith("/")) {
return firstPath.substr(0, firstPath.length - 1) + secondPath;
} else if (firstPath.endsWith("/") || secondPath.endsWith("/") || secondPath.startsWith("/")) {
return firstPath + secondPath;
} else {
return `${firstPath}/${secondPath}`;
}
}
}
/**
* Node class
*/
class Node
{
/**
* Creates an instance of a node. Do not use this constructor yourself. To add nodes, use the add and addChild methods
*
* @param {string} name The name of the node
* @param data Node data
* @param {Tree} tree The tree to which the node will belong
* @constructor
*/
constructor(name, data, tree) {
/**
* Name of this node
*/
this.name = name;
/**
* Data of this node
*/
this.data = data;
/** @private */
this._children = [];
this._tree = tree;
}
/**
* The tree to which the node will belong
*
* @returns {Tree} Tree to which the node will belong
* @this {Node}
*/
get tree() {
return this._tree;
}
/**
* Getter to get the path to this node
*
* @returns {string} The path to this node
* @this {Node} Node
* @throws {TreeError} In case this node does not belong to any tree
*/
get path() {
if (this.tree == null) {
throw new TreeError("This node does not belong to any tree");
}
const parentsArray = [];
function recurse(node) {
if (node != null) {
parentsArray.push(node);
return recurse(node.parent);
} else {
return parentsArray;
}
}
const pathArray = recurse(this);
pathArray.pop();
return `/${pathArray.map(node => node.name).reverse().join("/")}`;
}
/**
* Getter to get the parent of this node
*
* @returns {Node} Parent of this node
* @this {Node} Node
* @throws {TreeError} In case this node does not belong to any tree
*/
get parent() {
if (this.tree == null) {
throw new TreeError("This node does not belong to any tree");
}
if (this.tree.root._children.includes(this)) {
return this.tree.root;
}
function recurse(children) {
const parent = children.find((item) => item._children.includes(this));
if (parent != null) {
return parent;
} else {
for (const child of children) {
return recurse.call(this, child._children);
}
}
}
return recurse.call(this, this.tree.root._children);
}
/**
* A getter that returns an array of children of the given node
*
* @this {Node} Node
* @returns {Array} Array of children for this node
*/
get children() {
return this._children;
}
/**
* Deletes the given node and its children
*
* @this {Node} Node
* @throws {TreeError} In case this node does not belong to any tree
* @throws {TreeError} If this node is the root
*/
remove() {
if (this.tree == null) {
throw new TreeError("This node does not belong to any tree");
}
if (this.parent == null) {
throw new TreeError("Cannot remove root node");
} else {
this.parent._children.splice(this.parent._children.indexOf(this), 1);
this._tree = null;
this._children = null;
}
}
/**
* Adds a child to this node
*
* @param {string} name The name of the node to add
* @param data The data of the node being added
* @this {Node} Node
* @throws {TreeError} In case this node does not belong to any tree
* @throws {TreeError} In case the node already exists
*/
addChild(name, data) {
if (this.tree == null) {
throw new TreeError("This node does not belong to any tree")
}
if (this.tree.has(Tree.joinPath(this.path, name))) {
throw new TreeError("This node already exists");
}
const node = new Node(name, data, this.tree);
this._children.push(node);
return node;
}
/**
* Returns a node object suitable for storage in JSON format. This method is mainly used by the JSON.stringify function
*
* @this {Node} Node
* @returns {object} A node object suitable for storage in JSON format
* @example
* node.toJSON(); // { name: "root", data: { text: "Hello, world!", "otherText": "hoI!" }, children: [{ name: "node1", data: {text: "Hello, world!", "otherText": "hoI!" }, children: [{ name: "node2", data: { text: "Hello, world!", "otherText": "hoI!" }, children: [] }] }
*/
toJSON() {
return { name: this.name, data: this.data, children: this._children };
}
/**
* Calls a callback for each child node of this node
*
* @param {Function} callback A function called for child node of this node The node in the first argument is passed to the function
* @this {Node} Node
* @example
* node.traverse(node => {
* console.log(node.name);
* });
*/
traverse(callback) {
function recurse(node) {
callback(node);
for (const child of node._children) {
recurse(child);
}
}
recurse(this);
}
}
/**
* It is a TreeWithPath error class.
* @private
*/
class TreeError extends Error {
constructor(message) {
super(message);
this.name = "TreeError";
}
}
/** @private */
function parsePath(path) {
if (path.startsWith("/")) {
const pathArray = path.split("/");
pathArray[0] = "root";
if (pathArray[pathArray.length - 1] == "") {
pathArray.pop();
}
return pathArray;
} else {
throw new TreeError("Wrong path");
}
}
module.exports = Tree;