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.

129 lines
3.4 KiB
JavaScript

"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(_)
]);
}
};