"use strict"; const assureArray = require("assure-array"); const shallowMerge = require("../shallow-merge"); const syncpipe = require("syncpipe"); const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); const isString = require("@validatem/is-string"); const isArray = require("@validatem/is-array"); const ValidationError = require("@validatem/error"); function createListValidator() { let lastSequenceNumber = null; return function isTreecutterList(value) { isArray(value); if (value.some((item) => item._treecutterDepth == null || item._treecutterSequenceNumber == null)) { throw new ValidationError(`Must be a treecutter-generated list of items`); } else if (lastSequenceNumber != null && value._treecutterSequenceNumber !== lastSequenceNumber + 1) { throw new ValidationError(`Must be the original, unfiltered, unsorted treecutter-generated list of items`); } else { lastSequenceNumber = value._treecutterSequenceNumber; } }; } let validateTreecutterOptions = { childrenProperty: isString }; function defaultOptions(options = {}) { return { childrenProperty: options.childrenProperty ?? "children" }; } module.exports = { flatten: function (tree, options) { validateArguments(arguments, { tree: [ required ], options: [ validateTreecutterOptions ] }); let { childrenProperty } = defaultOptions(options); let rootItems = assureArray(tree); let list = []; let sequenceNumber = 0; function add(items, depth) { for (let item of items) { let listItem = shallowMerge(item, { _treecutterDepth: depth, _treecutterSequenceNumber: sequenceNumber }); // listItem is a copy, so we can do this safely delete listItem[childrenProperty]; list.push(listItem); sequenceNumber += 1; if (item[childrenProperty] != null) { add(item[childrenProperty], depth + 1); } } } add(rootItems, 0); return list; }, rebuild: function (list, options) { let isTreecutterList = createListValidator(); validateArguments(arguments, { list: [ required, isTreecutterList ], options: [ validateTreecutterOptions ] }); let { childrenProperty } = defaultOptions(options); let topLevel = []; let stack = []; let currentDepth = list[0]?._treecutterDepth; for (let item of list) { let depth = item._treecutterDepth; let treeItem = shallowMerge(item, { [childrenProperty]: [] }); // Again, we're operating on a copy. delete treeItem._treecutterDepth; delete treeItem._treecutterSequenceNumber; if (depth >= 0 && depth <= currentDepth + 1) { if (depth === 0) { topLevel.push(treeItem); } else { stack[depth - 1][childrenProperty].push(treeItem); } currentDepth = depth; stack[depth] = treeItem; stack.splice(depth + 1); // Remove references higher in the stack, to decrease the chance of a silent failure if there's a bug in the code } else { throw new Error(`Encountered an invalid item depth; the item's depth is ${depth}, but the current tree depth is ${currentDepth}; if this list was generated by treecutter, please file a bug!`); } } return topLevel; }, map: function (tree, mapFunc) { return syncpipe(tree, [ (_) => this.flatten(_), (_) => _.map((item) => ({ ... mapFunc(item), _treecutterDepth: item._treecutterDepth, _treecutterSequenceNumber: item._treecutterSequenceNumber, })), (_) => this.rebuild(_) ]); } };