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.

139 lines
3.2 KiB
JavaScript

"use strict";
// FIXME: Publish as stand-alone `reference-tree` package, clearly document that it is not order-sensitive
function isObject(value) {
return (value != null && typeof value === "object");
}
function isLabelReference(value) {
return (isObject(value) && value.__referenceTree_reference != null);
}
function isParentReference(value) {
return (isObject(value) && value.__referenceTree_parent != null);
}
function hasLabel(value) {
return (isObject(value) && value.__referenceTree_label != null);
}
module.exports = {
build: function buildReferenceTree(object) {
// NOTE: Mutates input object!
let stack = [];
let labelledItems = new Map();
let seen = new Set();
let onFirstPass = true;
let doSecondPass = false;
function handleItem(container, key) {
let value = container[key];
if (!seen.has(value)) {
seen.add(value);
if (hasLabel(value)) {
let label = value.__referenceTree_label;
delete value.__referenceTree_label;
labelledItems.set(label, value);
}
if (isLabelReference(value)) {
let label = value.__referenceTree_reference;
if (labelledItems.has(label)) {
container[key] = labelledItems.get(label);
} else if (onFirstPass) {
// We haven't seen the referenced item yet; this typically happens when either the reference is defined before the label, or when there are cyclical references
doSecondPass = true;
} else {
throw new Error(`Encountered a label '${label}' that doesn't exist anywhere in the tree`);
}
} else if (isParentReference(value)) {
let levels = value.__referenceTree_parent;
if (levels > stack.length) {
throw new Error(`Tried to access a parent ${levels} steps away, but there are only ${stack.length} parents`);
} else {
let parentIndex = stack.length - levels;
container[key] = stack[parentIndex];
}
} else {
stack.push(container);
processValue(value);
stack.pop();
}
}
}
function processObject(object) {
for (let key of Object.keys(object)) {
handleItem(object, key);
}
}
function processArray(array) {
for (let i = 0; i < array.length; i++) {
handleItem(array, i);
}
}
function processValue(value) {
if (Array.isArray(value)) {
return processArray(value);
} else if (isObject(value)) {
return processObject(value);
}
}
processValue(object);
if (doSecondPass === true) {
onFirstPass = false;
seen = new Set();
processValue(object);
}
return object;
},
Parent: function (levels = 1) {
return {
__referenceTree_parent: levels
};
},
Label: function (label, object) {
object.__referenceTree_label = label;
return object;
},
Reference: function (label) {
return {
__referenceTree_reference: label
};
}
};
// let { build, Label, Reference, Parent } = module.exports;
// let result = build({
// foo: "bar",
// a: Label("A", {
// letter: "A",
// foo: Reference("B")
// }),
// b: Label("B", {
// letter: "B",
// foo: Reference("A"),
// children: [{
// qux: "quz"
// }, {
// qux: Parent(2),
// quz: Reference("B")
// }]
// }),
// });
// console.dir(result, { depth: 4 });
// console.log(result.b.children[1].qux === result.b.children[1].quz);