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