start impl rrdom

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 2906b0cb22
commit ab819bf338
2 changed files with 225 additions and 0 deletions

173
src/rrdom/index.ts Normal file
View File

@@ -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());
}
}

52
src/rrdom/tree-node.ts Normal file
View File

@@ -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;
}
}