You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

112 lines
2.9 KiB
JavaScript

"use strict";
const { validateArguments, required, isString, isArray, ValidationError } = require("validatem");
const assureArray = require("assure-array");
const shallowMerge = require("../shallow-merge");
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;
}
};