UNPKG

fast-tree-builder

Version:

Easily construct highly customizable bi-directional tree structures from iterable data.

219 lines (218 loc) 8.09 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var index_exports = {}; __export(index_exports, { default: () => buildTree }); module.exports = __toCommonJS(index_exports); function buildTree(items, options) { if (!options) { throw new Error(`Missing required 'options' parameter.`); } const { id: idAccessor, parentId: parentIdAccessor, childIds: childIdsAccessor, valueResolver, valueKey = "value", parentKey = "parent", childrenKey = "children", depthKey = false, validateTree = false, validateReferences = false, includeEmptyChildrenArray = false } = options; if (!idAccessor) { throw new Error(`Option 'id' is required.`); } if (!parentIdAccessor && !childIdsAccessor) { throw new Error(`Either 'parentId' or 'childIds' must be provided.`); } if (parentIdAccessor && childIdsAccessor) { throw new Error(`'parentId' and 'childIds' cannot be used together.`); } const roots = []; const nodes = /* @__PURE__ */ new Map(); if (parentIdAccessor) { const waitingForParent = /* @__PURE__ */ new Map(); for (const item of items) { const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor]; if (nodes.has(id)) { throw new Error(`Duplicate identifier '${id}'.`); } const node = valueKey !== false ? { [valueKey]: valueResolver ? valueResolver(item) : item } : { ...valueResolver ? valueResolver(item) : item }; if (valueKey === false) { if (parentKey !== false) { delete node[parentKey]; } delete node[childrenKey]; } if (includeEmptyChildrenArray) { node[childrenKey] = []; } nodes.set(id, node); const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor]; const parentNode = nodes.get(parentId); if (parentNode) { parentNode[childrenKey] ||= []; parentNode[childrenKey].push(node); if (parentKey !== false) { node[parentKey] = parentNode; } } else { const siblings = waitingForParent.get(parentId); if (siblings) { siblings.push(node); } else { waitingForParent.set(parentId, [node]); } } const children = waitingForParent.get(id); if (children) { node[childrenKey] = children; if (parentKey !== false) { for (const child of children) { child[parentKey] = node; } } waitingForParent.delete(id); } } for (const [parentId, nodes2] of waitingForParent.entries()) { if (validateReferences && parentId != null) { throw new Error(`Referential integrity violation: parentId '${parentId}' does not match any item in the input.`); } for (const node of nodes2) { roots.push(node); } } } else if (childIdsAccessor) { const waitingChildren = /* @__PURE__ */ new Map(); const rootCandidates = /* @__PURE__ */ new Map(); for (const item of items) { const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor]; if (nodes.has(id)) { throw new Error(`Duplicate identifier '${id}'.`); } const node = valueKey !== false ? { [valueKey]: valueResolver ? valueResolver(item) : item } : { ...valueResolver ? valueResolver(item) : item }; if (valueKey === false) { if (parentKey !== false) { delete node[parentKey]; } delete node[childrenKey]; } if (includeEmptyChildrenArray) { node[childrenKey] = []; } nodes.set(id, node); const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor]; if (childIds != null) { if (typeof childIds[Symbol.iterator] !== "function") { throw new Error(`Item '${id}' has invalid children: expected an iterable value.`); } node[childrenKey] = []; for (const childId of childIds) { const childNode = nodes.get(childId); if (childNode) { node[childrenKey].push(childNode); if (parentKey !== false) { if (childNode[parentKey] && childNode[parentKey] !== node) { throw new Error(`Node '${childId}' already has a different parent, refusing to overwrite.`); } childNode[parentKey] = node; } rootCandidates.delete(childId); } else { if (waitingChildren.has(childId)) { throw new Error(`Multiple parents reference the same unresolved child '${childId}'.`); } waitingChildren.set(childId, { parentNode: node, childIndex: node[childrenKey].length }); node[childrenKey].push(null); } } if (node[childrenKey].length === 0) { delete node[childrenKey]; } } const parentDescriptor = waitingChildren.get(id); if (parentDescriptor) { const { parentNode, childIndex } = parentDescriptor; parentNode[childrenKey][childIndex] = node; if (parentKey !== false) { if (node[parentKey] && node[parentKey] !== parentNode) { throw new Error(`Node '${id}' already has a different parent, refusing to overwrite.`); } node[parentKey] = parentNode; } waitingChildren.delete(id); } else { rootCandidates.set(id, node); } } if (waitingChildren.size > 0) { if (validateReferences) { const childId = waitingChildren.keys().next().value; throw new Error(`Referential integrity violation: child reference '${childId}' does not match any item in the input.`); } const pending = Array.from(waitingChildren.values()); for (let i = pending.length - 1; i >= 0; i--) { const { parentNode, childIndex } = pending[i]; parentNode[childrenKey].splice(childIndex, 1); if (parentNode[childrenKey].length === 0) { delete parentNode[childrenKey]; } } } for (const node of rootCandidates.values()) { roots.push(node); } } const withDepth = typeof depthKey === "string" || typeof depthKey === "symbol" || typeof depthKey === "number"; if (validateTree || withDepth) { if (roots.length === 0 && nodes.size > 0) { throw new Error("Tree validation failed: detected a cycle."); } const stack = [...roots].map((node) => ({ node, depth: 0 })); const visited = /* @__PURE__ */ new Set(); let processedCount = 0; const MAX_NODES = nodes.size; while (stack.length > 0 && processedCount++ <= MAX_NODES) { const { node, depth } = stack.pop(); if (visited.has(node)) { throw new Error("Tree validation failed: a node reachable via multiple paths."); } visited.add(node); if (withDepth) { node[depthKey] = depth; } if (node[childrenKey]) { for (const child of node[childrenKey]) { stack.push({ node: child, depth: depth + 1 }); } } } if (nodes.size !== visited.size) { throw new Error("Tree validation failed: detected a cycle."); } } return { roots, nodes }; }