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
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;
|
|
}
|
|
};
|