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