From ab819bf338640638fe9800451857ccf63e81f0c7 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] start impl rrdom --- src/rrdom/index.ts | 173 +++++++++++++++++++++++++++++++++++++++++ src/rrdom/tree-node.ts | 52 +++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/rrdom/index.ts create mode 100644 src/rrdom/tree-node.ts diff --git a/src/rrdom/index.ts b/src/rrdom/index.ts new file mode 100644 index 00000000..28b5e590 --- /dev/null +++ b/src/rrdom/index.ts @@ -0,0 +1,173 @@ +import { RRdomTreeNode, AnyObject } from './tree-node'; + +class RRdomTree { + private readonly symbol = '__rrdom__'; + + public initialize(object: AnyObject) { + this._node(object); + + return object; + } + + public hasChildren(object: AnyObject): boolean { + return Boolean(this._node(object).hasChildren); + } + + public firstChild(object: AnyObject) { + return this._node(object).firstChild || null; + } + + public lastChild(object: AnyObject) { + return this._node(object).lastChild || null; + } + + public previousSibling(object: AnyObject) { + return this._node(object).previousSibling || null; + } + + public nextSibling(object: AnyObject) { + return this._node(object).nextSibling || null; + } + + public parent(object: AnyObject) { + return this._node(object).parent || null; + } + + public insertAfter(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const nextNode = this._node(referenceNode.nextSibling); + const newNode = this._node(newObject); + const parentNode = this._node(referenceNode.parent); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + newNode.parent = referenceNode.parent; + newNode.previousSibling = referenceObject; + newNode.nextSibling = referenceNode.nextSibling; + referenceNode.nextSibling = newObject; + + if (nextNode) { + nextNode.previousSibling = newObject; + } + + if (parentNode && parentNode.lastChild === referenceObject) { + parentNode.lastChild = newObject; + } + + if (parentNode) { + parentNode.childrenChanged(); + } + + return newObject; + } + + public insertBefore(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const prevNode = this._node(referenceNode.previousSibling); + const newNode = this._node(newObject); + const parentNode = this._node(referenceNode.parent); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + newNode.parent = referenceNode.parent; + newNode.previousSibling = referenceNode.previousSibling; + newNode.nextSibling = referenceObject; + referenceNode.previousSibling = newObject; + + if (prevNode) { + prevNode.nextSibling = newObject; + } + + if (parentNode && parentNode.firstChild === referenceObject) { + parentNode.firstChild = newObject; + } + + if (parentNode) { + parentNode.childrenChanged(); + } + + return newObject; + } + + public appendChild(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const newNode = this._node(newObject); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + if (referenceNode.hasChildren) { + this.insertAfter(referenceNode.lastChild!, newObject); + } else { + newNode.parent = referenceObject; + referenceNode.firstChild = newObject; + referenceNode.lastChild = newObject; + referenceNode.childrenChanged(); + } + + return newObject; + } + + public remove(removeObject: AnyObject) { + const removeNode = this._node(removeObject); + const parentNode = this._node(removeNode.parent); + const prevNode = this._node(removeNode.previousSibling); + const nextNode = this._node(removeNode.nextSibling); + + if (parentNode) { + if (parentNode.firstChild === removeObject) { + parentNode.firstChild = removeNode.nextSibling; + } + + if (parentNode.lastChild === removeObject) { + parentNode.lastChild = removeNode.previousSibling; + } + } + + if (prevNode) { + prevNode.nextSibling = removeNode.nextSibling; + } + + if (nextNode) { + nextNode.previousSibling = removeNode.previousSibling; + } + + removeNode.parent = null; + removeNode.previousSibling = null; + removeNode.nextSibling = null; + removeNode.cachedIndex = -1; + removeNode.cachedIndexVersion = NaN; + + if (parentNode) { + parentNode.childrenChanged(); + } + + return removeObject; + } + + private _node(object: AnyObject | null): RRdomTreeNode { + if (!object) { + throw new Error('Object is falsy'); + } + + if (this.symbol in object) { + return object[this.symbol] as RRdomTreeNode; + } + + return (object[this.symbol] = new RRdomTreeNode()); + } +} diff --git a/src/rrdom/tree-node.ts b/src/rrdom/tree-node.ts new file mode 100644 index 00000000..828e0aa8 --- /dev/null +++ b/src/rrdom/tree-node.ts @@ -0,0 +1,52 @@ +// tslint:disable-next-line: no-any +export type AnyObject = { [key: string]: any; __rrdom__?: RRdomTreeNode }; + +export class RRdomTreeNode implements AnyObject { + public parent: AnyObject | null = null; + public previousSibling: AnyObject | null = null; + public nextSibling: AnyObject | null = null; + + public firstChild: AnyObject | null = null; + public lastChild: AnyObject | null = null; + + // This value is incremented anytime a children is added or removed + public childrenVersion = 0; + // The last child object which has a cached index + public childIndexCachedUpTo: AnyObject | null = null; + + /** + * This value represents the cached node index, as long as + * cachedIndexVersion matches with the childrenVersion of the parent + */ + public cachedIndex = -1; + public cachedIndexVersion = NaN; + + public get isAttached() { + return Boolean(this.parent || this.previousSibling || this.nextSibling); + } + + public get hasChildren() { + return Boolean(this.firstChild); + } + + public childrenChanged() { + // tslint:disable-next-line: no-bitwise + this.childrenVersion = (this.childrenVersion + 1) & 0xffffffff; + this.childIndexCachedUpTo = null; + } + + public getCachedIndex(parentNode: AnyObject) { + if (this.cachedIndexVersion !== parentNode.childrenVersion) { + this.cachedIndexVersion = NaN; + // cachedIndex is no longer valid + return -1; + } + + return this.cachedIndex; + } + + public setCachedIndex(parentNode: AnyObject, index: number) { + this.cachedIndexVersion = parentNode.childrenVersion; + this.cachedIndex = index; + } +}