UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

249 lines (216 loc) 7.72 kB
// json-node.js export class JSONNode { constructor(value, parent = null, keyOrIndex = null, instanceId = 'default') { this.value = value; this.parent = parent; this.keyOrIndex = keyOrIndex; this.instanceId = instanceId; this.__jsonlens__ = true; if (Array.isArray(value)) { this.children = value.map((child, index) => new JSONNode(child, this, index, instanceId)); } else if (typeof value === 'object' && value !== null) { this.children = Object.entries(value).map( ([key, val]) => new JSONNode(val, this, key, instanceId), ); } else { this.children = []; } } // Methods for JSONDomFacade compatibility getParent() { return this.parent; } getChildren() { return this.children; } getKey() { return this.keyOrIndex; } getValue() { return this.value; } /** * Get the value of this node or a child node by key. * @param {string|number} [key] - Optional key or index to get a child node * @returns {*|JSONNode|undefined} The raw value of this node or a child node */ get(key) { // If no key is provided, return the raw value of this node if (arguments.length === 0) { return this.value; } // Otherwise, get child node by key or index if (Array.isArray(this.value) && typeof key === 'number') { return this.children[key]; } if (typeof this.value === 'object' && this.value !== null) { return this.children.find(c => c.keyOrIndex === key); } return undefined; } /** * Get the XPath-style path to this node including instance prefix. * @returns {string|null} */ getPath() { // Detached: no parent and no key const isDetached = this.parent === null && this.keyOrIndex === null; // Special case: this is NOT the original root (value-only root has no children) if (isDetached && this.children?.length === 0) { return null; } const segments = []; let node = this; while (node.parent !== null) { const key = node.keyOrIndex; if (key === null || key === undefined) return null; if (typeof key === 'number') { segments.unshift(`[${key + 1}]`); } else if (/^[a-zA-Z_][\w\-]*$/.test(key)) { segments.unshift(`/${key}`); } else { const escaped = String(key).replace(/'/g, `''`); segments.unshift(`/'${escaped}'`); } node = node.parent; } return `$${this.instanceId}${segments.join('')}`; } /** * Set the raw value of this node. * @param {*} value */ set(value) { this.value = value; // Update the parent's data structure if this is a child node if (this.parent && this.keyOrIndex !== null && this.keyOrIndex !== undefined) { this.parent.value[this.keyOrIndex] = value; } this.children = []; if (Array.isArray(value)) { this.children = value.map( (child, index) => new JSONNode(child, this, index, this.instanceId), ); } else if (typeof value === 'object' && value !== null) { this.children = Object.entries(value).map( ([key, val]) => new JSONNode(val, this, key, this.instanceId), ); } } /** * Insert into array or object node. * @param {*} value * @param {string|number|null} keyOrIndex */ insert(value, keyOrIndex = null) { if (Array.isArray(this.value)) { const index = keyOrIndex === null ? this.value.length : keyOrIndex; this.value.splice(index, 0, value); this.children.splice(index, 0, new JSONNode(value, this, index, this.instanceId)); // update keyOrIndex of all children after insertion for (let i = index + 1; i < this.children.length; i++) { this.children[i].keyOrIndex = i; } } else if (typeof this.value === 'object') { if (typeof keyOrIndex !== 'string') { throw new Error('Insert into object requires a string key.'); } this.value[keyOrIndex] = value; this.children.push(new JSONNode(value, this, keyOrIndex, this.instanceId)); } else { throw new Error('Cannot insert into primitive value.'); } } delete() { if (!this.parent || this.keyOrIndex == null) return; const parentVal = this.parent.value; if (Array.isArray(parentVal)) { parentVal.splice(this.keyOrIndex, 1); this.parent.set(parentVal); } else if (typeof parentVal === 'object' && parentVal !== null) { delete parentVal[this.keyOrIndex]; this.parent.set(parentVal); } else { // Parent is not deletable (primitive etc.) throw new Error('Parent is not an object or array — cannot delete child'); } } } /** * Wrap a raw JSON value as a lensable node tree. * @param {*} value * @param {*} parent * @param {*} keyOrIndex * @param {string} instanceId * @returns {JSONNode} */ export function wrapJson(value, parent = null, keyOrIndex = null, instanceId = 'default') { const jsonNode = new JSONNode(value, parent, keyOrIndex, instanceId); console.log('wrapJson', jsonNode); return jsonNode; // return new JSONNode(value, parent, keyOrIndex, instanceId); } /** * Returns the correct lens for a JSON node based on its parent and key/index. * Assumes the node is already in a wrapped structure, or retrievable via parent. * * @param {any} node - The JSON node (primitive value or structure) * @param {JSONNode} parent - The parent JSONNode * @param {string|number} keyOrIndex - The key (for objects) or index (for arrays) * @returns {JSONNode} A JSONNode wrapping the value with lens and context */ /** * Returns the correct lens for a JSON node based on its parent and key/index. * Assumes the node is already in a wrapped structure, or retrievable via parent. * * @param {any} node - The JSON node (primitive value or structure) * @param {JSONNode} parent - The parent JSONNode * @param {string|number} keyOrIndex - The key (for objects) or index (for arrays) * @returns {JSONNode} A JSONNode wrapping the value with lens and context */ /* export function getLensForNode(node, parent, keyOrIndex) { if (!parent || !parent.__jsonlens__) { console.warn('getLensForNode called without proper parent lens'); return node; } const lens = parent.__jsonlens__.lens; const wrapped = lens.get(parent.value, keyOrIndex); return wrapped; } */ /** * Attempts to wrap a raw value into a JSONNode, using fallback logic if needed. * @param {any} value - The raw value to wrap. * @param {JSONNode|null} parent - The parent JSONNode (if known). * @param {string|number|null} key - The key/index in the parent (if known). * @param {string} instanceId - The ID of the instance. * @param {Object} instanceRoot - The full wrapped instance root, to help recover parent/key. * @returns {JSONNode|any} */ export function getLensForNode(value, parent = null, key = null, instanceId, instanceRoot = null) { if (value?.__jsonlens__) return value; // Attempt fallback recovery if key or parent missing if ((!parent || typeof key === 'undefined') && instanceRoot) { const queue = [instanceRoot]; while (queue.length) { const current = queue.shift(); if (!current?.children) continue; for (const child of current.children) { if (child.value === value) { parent = current; key = child.keyOrIndex; break; } else { queue.push(child); } } if (parent) break; } if (!parent || typeof key === 'undefined') { console.warn('[getLensForNode] Unable to determine parent/key for value:', value); return value; // Bail out, return raw value } } return new JSONNode(value, parent, key, instanceId); }