@stackend/api
Version:
JS bindings to api.stackend.com
431 lines (365 loc) • 8.97 kB
text/typescript
import { generatePermalink } from './permalink';
import Reference from './Reference';
import XcapObject from './XcapObject';
export const TREE_CLASS = 'se.josh.xcap.tree.impl.TreeImpl';
export interface Node {
permalink: string;
name: string;
description: string;
ref: Reference | null /** Referenced object */;
referenceId: number /** Id of referenced object */;
data: any /** user data */;
children: Array<Node>;
}
export interface Tree extends XcapObject, Node {
totalChildNodes: number;
}
/**
* Create, but does not store a new tree
*/
export function newTree(name: string): Tree {
const permalink = generatePermalink(name);
if (!permalink) {
throw Error('Could not generate permalink');
}
return {
__type: TREE_CLASS,
id: 0,
permalink,
name,
description: '',
ref: null,
children: [],
data: {},
totalChildNodes: 0,
referenceId: 0
};
}
/**
* Create, but does not store a new node
*/
export function newTreeNode(name: string): Node {
const permalink = generatePermalink(name);
if (!permalink) {
throw Error('Could not generate permalink');
}
return {
name,
permalink,
description: '',
ref: null,
children: [],
data: {},
referenceId: 0
};
}
/**
* Clone a node
* @param node
* @returns {Node|*}
*/
export function cloneNode(node: Node): Node {
if (node == null) {
return node;
}
const children = [];
for (let i = 0; i < node.children.length; i++) {
const c: Node = node.children[i];
children.push(cloneNode(c));
}
return Object.assign({}, node, { children });
}
/**
* Clone a tree
* @param tree
* @returns {Node}
*/
export function cloneTree(tree: Tree): Tree {
return cloneNode(tree) as Tree;
}
function _forEachNode(
node: Node,
parents: Array<Node>,
apply: (node: Node, parent: Node, parents: Array<Node>, i: number) => boolean
): Array<Node> | null {
parents.push(node);
for (let i = 0; i < node.children.length; i++) {
const n = node.children[i];
if (apply(n, node, parents, i)) {
parents.push(n);
return parents;
}
const r = _forEachNode(n, parents, apply);
if (r) {
return r;
}
}
parents.pop();
return null;
}
/**
* Get the permalink to a three path
* @param treePath
* @param node
*/
export function getPermalink(treePath: Array<Node>, node?: Node | null): string {
let s = '';
for (let i = 0; i < treePath.length; i++) {
s += '/' + treePath[i].permalink;
}
if (node) {
s += '/' + node.permalink;
}
return s;
}
function makeNodePermalinksUnique(children: Array<Node>): void {
const pls: Set<string> = new Set();
for (let i = 0; i < children.length; i++) {
const n: Node = children[i];
const pl = n.permalink || 'page';
let x = pl;
let j = 0;
while (true) {
if (!pls.has(x)) {
n.permalink = x;
break;
}
j++;
x = pl + '-' + j;
}
pls.add(n.permalink);
makeNodePermalinksUnique(n.children);
}
}
/**
* Get an array with all parent nodes up to a specific node
* @param tree
* @param node
*/
export function getTreePath(tree: Tree | null, node: Node | null): Array<Node> | null {
if (!tree || !node) {
return null;
}
return _forEachNode(tree, [], (n: Node) => node === n);
}
export function getTreePathMatch(
tree: Tree,
apply: (node: Node, parent: Node, parents: Array<Node>, i: number) => boolean
): Array<Node> | null {
if (!tree) {
return null;
}
return _forEachNode(tree, [], apply);
}
/**
* Get a tree node by its permalink
* @param tree
* @param permalink
*/
export function getTreeNodeByPermalink(tree: Tree, permalink: string | null): Node | null {
if (!tree || !permalink) {
return null;
}
const p = permalink.replace(/^\//, '').replace(/\/$/, '');
if (!p) {
return null;
}
return _findNodeByPermalink(tree, p);
}
export function _findNodeByPermalink(tree: Tree, permalink: string, accumulatedPermalink?: string): Node | null {
if (!tree) {
return null;
}
for (let i = 0; i < tree.children.length; i++) {
const n = tree.children[i];
const p = (accumulatedPermalink ? accumulatedPermalink + '/' : '') + n.permalink;
if (permalink == p) {
return n;
}
const f = _findNodeByPermalink(n as Tree, permalink, p);
if (f) {
return f;
}
}
return null;
}
/**
* Remove a tree node
* @param tree
* @param node
* @returns {boolean}
*/
export function removeTreeNode(tree: Tree, node: Node): Node | null {
if (tree == null || node == null) {
return null;
}
const r = _forEachNode(tree, [], (n: Node, parent: Node, parents: Array<Node>, i: number) => {
if (n === node) {
parent.children.splice(i, 1);
return true;
}
return false;
});
return r === null ? null : r[r.length - 1];
}
export function removeTreeNodeByPermalink(tree: Tree, permalink: string): Node | null {
if (tree == null || !permalink) {
return null;
}
const r = _forEachNode(tree, [], (n: Node, parent: Node, parents: Array<Node>, i: number) => {
const pl = getPermalink(parents, n);
if (pl === permalink) {
parent.children.splice(i, 1);
return true;
}
return false;
});
return r == null ? null : r[r.length - 1];
}
export enum InsertionPoint {
BEFORE = 'BEFORE',
AFTER = 'AFTER',
CHILD = 'CHILD'
}
/**
* Move a tree node
* @param tree
* @param node
* @param insertionPoint
* @param relativeTo
*/
export function moveTreeNode(tree: Tree, node: Node, insertionPoint: InsertionPoint, relativeTo: Node): boolean {
console.assert(tree);
console.assert(node);
console.assert(insertionPoint);
console.assert(relativeTo);
removeTreeNode(tree, node);
if (insertionPoint === InsertionPoint.CHILD) {
relativeTo.children.push(node);
makeNodePermalinksUnique(relativeTo.children);
return true;
}
// Insertion before or after
const parents = getTreePath(tree, relativeTo);
if (!parents) {
return false; // Should not happen
}
const insertionParent = parents[parents.length - 2];
const idx = insertionParent.children.findIndex(c => c === relativeTo);
if (idx === -1) {
return false;
}
const i = insertionPoint === InsertionPoint.BEFORE ? idx : idx + 1;
insertionParent.children.splice(i, 0, node);
makeNodePermalinksUnique(insertionParent.children);
return true;
}
/**
* Add a node to the tree
* @param tree
* @param node
*/
export function addNode(tree: Tree, node: Node): Tree | null {
if (!tree || !node) {
return null;
}
tree.children = tree.children.concat(node);
makeNodePermalinksUnique(tree.children);
return tree;
}
/**
* Find a node that matches the test
* @param tree
* @param test
* @returns {Node|null|?Node}
*/
export function findNode(tree: Tree, test: (node: Node) => boolean): Node | null {
if (!tree) {
return null;
}
for (let i = 0; i < tree.children.length; i++) {
const n = tree.children[i];
if (test(n)) {
return n;
}
const f = findNode(n as Tree, test);
if (f) {
return f;
}
}
return null;
}
/**
* Get the tree path to a node
* @param tree
* @param permalink
* @returns {?Array<Node>}
*/
export function getNodePath(tree: Tree, permalink: string): Array<Node> | null {
return _forEachNode(tree, [], (node: Node, parent: Node, parents: Array<Node>) => {
return getPermalink(parents, node) === permalink;
});
}
/**
* Get a node by it's permalink
* @param tree
* @param permalink
*/
export function getNode(tree: Tree, permalink: string): Node | null {
const path = getNodePath(tree, permalink);
if (!path) {
return null;
}
return path[path.length - 1];
}
/**
* Apply a function to each node
* @param tree
* @param apply A function to apply. Returning true will abort the processing
* @return Path to the node where the iteration was aborted
*/
export function forEachNode(
tree: Tree,
apply: (node: Node, parent: Node, parents: Array<Node>, i: number) => boolean
): Array<Node> | null {
if (!tree) {
return null;
}
return _forEachNode(tree, [], apply);
}
/**
* Check if the path is a sub path of ofPath
* @param path
* @param ofPath
* @returns {boolean}
*/
export function isSubPath(path: Array<Node>, ofPath: Array<Node>): boolean {
if (!path || !ofPath || path.length > ofPath.length) {
return false;
}
for (let i = 0; i < path.length; i++) {
if (ofPath[i].permalink !== path[i].permalink) {
return false;
}
}
return true;
}
/**
* Get the tree part of the permalink
* @param permalink
* @returns {string|null}
*/
export function getTreePermalink(permalink: string | null): string | null {
if (!permalink) {
return null;
}
let pl = permalink;
if (pl.startsWith('/')) {
pl = pl.substring(1);
}
const i = pl.indexOf('/');
if (i !== -1) {
pl = pl.substring(0, i);
}
return pl;
}