basic rebuild implementation

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 97c4b4f6e1
commit a71fb73aaf
4 changed files with 193 additions and 137 deletions

View File

@@ -1,138 +1,4 @@
let _id = 1;
import snapshot from './snapshot';
import rebuild from './rebuild';
function genId(): number {
return _id++;
}
enum NodeType {
Document,
DocumentType,
Element,
Text,
CDATA,
Comment,
}
type serializedNode =
| documentNode
| documentTypeNode
| elementNode
| textNode
| cdataNode
| commentNode;
type documentNode = {
type: NodeType.Document;
childNodes: serializedNode[];
};
type documentTypeNode = {
type: NodeType.DocumentType;
name: string;
publicId: string;
systemId: string;
};
type attributes = {
[key: string]: string;
};
type elementNode = {
type: NodeType.Element;
tagName: string;
attributes: attributes;
childNodes: serializedNode[];
};
type textNode = {
type: NodeType.Text;
textContent: string;
};
type cdataNode = {
type: NodeType.CDATA;
textContent: '';
};
type commentNode = {
type: NodeType.Comment;
textContent: string;
};
function serializeNode(n: Node): serializedNode | false {
switch (n.nodeType) {
case n.DOCUMENT_NODE:
return {
type: NodeType.Document,
childNodes: [],
};
case n.DOCUMENT_TYPE_NODE:
return {
type: NodeType.DocumentType,
name: (n as DocumentType).name,
publicId: (n as DocumentType).publicId,
systemId: (n as DocumentType).systemId,
};
case n.ELEMENT_NODE:
const tagName = (n as HTMLElement).tagName.toLowerCase();
const attributes: attributes = {};
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
attributes[name] = value;
}
return {
type: NodeType.Element,
tagName,
attributes,
childNodes: [],
};
case n.TEXT_NODE:
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parentTagName =
n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = (n as Text).textContent;
if (parentTagName === 'SCRIPT') {
textContent = '';
}
return {
type: NodeType.Text,
textContent,
};
case n.CDATA_SECTION_NODE:
return {
type: NodeType.CDATA,
textContent: '',
};
case n.COMMENT_NODE:
return {
type: NodeType.Comment,
textContent: (n as Comment).textContent,
};
default:
return false;
}
}
type serializedNodeWithId = serializedNode & { id: number };
function snapshot(n: Node): serializedNodeWithId | null {
const _serializedNode = serializeNode(n);
if (!_serializedNode) {
// TODO: dev only
console.warn(n, 'not serialized');
return null;
}
const serializedNode: serializedNodeWithId = Object.assign(_serializedNode, {
id: genId(),
});
if (
serializedNode.type === NodeType.Document ||
serializedNode.type === NodeType.Element
) {
for (const childN of Array.from(n.childNodes)) {
serializedNode.childNodes.push(snapshot(childN));
}
}
return serializedNode;
}
export default snapshot;
export { snapshot, rebuild };

46
src/rebuild.ts Normal file
View File

@@ -0,0 +1,46 @@
import { serializedNodeWithId, NodeType } from './types';
function buildNode(n: serializedNodeWithId): Node | null {
switch (n.type) {
case NodeType.Document:
return document.implementation.createDocument(null, '', null);
case NodeType.DocumentType:
return document.implementation.createDocumentType(
n.name,
n.publicId,
n.systemId,
);
case NodeType.Element:
const node = document.createElement(n.tagName);
for (const name in n.attributes) {
if (n.attributes.hasOwnProperty(name)) {
node.setAttribute(name, n.attributes[name]);
}
}
return node;
case NodeType.Text:
return document.createTextNode(n.textContent);
case NodeType.CDATA:
return document.createCDATASection(n.textContent);
case NodeType.Comment:
return document.createComment(n.textContent);
default:
return null;
}
}
function rebuild(n: serializedNodeWithId): Node | null {
const root = buildNode(n);
if (!root) {
return null;
}
if (n.type === NodeType.Document || n.type === NodeType.Element) {
for (const childN of n.childNodes) {
const childNode = rebuild(childN);
root.appendChild(childNode);
}
}
return root;
}
export default rebuild;

89
src/snapshot.ts Normal file
View File

@@ -0,0 +1,89 @@
import {
serializedNode,
serializedNodeWithId,
NodeType,
attributes,
} from './types';
let _id = 1;
function genId(): number {
return _id++;
}
function serializeNode(n: Node): serializedNode | false {
switch (n.nodeType) {
case n.DOCUMENT_NODE:
return {
type: NodeType.Document,
childNodes: [],
};
case n.DOCUMENT_TYPE_NODE:
return {
type: NodeType.DocumentType,
name: (n as DocumentType).name,
publicId: (n as DocumentType).publicId,
systemId: (n as DocumentType).systemId,
};
case n.ELEMENT_NODE:
const tagName = (n as HTMLElement).tagName.toLowerCase();
const attributes: attributes = {};
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
attributes[name] = value;
}
return {
type: NodeType.Element,
tagName,
attributes,
childNodes: [],
};
case n.TEXT_NODE:
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parentTagName =
n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = (n as Text).textContent;
if (parentTagName === 'SCRIPT') {
textContent = '';
}
return {
type: NodeType.Text,
textContent,
};
case n.CDATA_SECTION_NODE:
return {
type: NodeType.CDATA,
textContent: '',
};
case n.COMMENT_NODE:
return {
type: NodeType.Comment,
textContent: (n as Comment).textContent,
};
default:
return false;
}
}
function snapshot(n: Node): serializedNodeWithId | null {
const _serializedNode = serializeNode(n);
if (!_serializedNode) {
// TODO: dev only
console.warn(n, 'not serialized');
return null;
}
const serializedNode: serializedNodeWithId = Object.assign(_serializedNode, {
id: genId(),
});
if (
serializedNode.type === NodeType.Document ||
serializedNode.type === NodeType.Element
) {
for (const childN of Array.from(n.childNodes)) {
serializedNode.childNodes.push(snapshot(childN));
}
}
return serializedNode;
}
export default snapshot;

55
src/types.ts Normal file
View File

@@ -0,0 +1,55 @@
export enum NodeType {
Document,
DocumentType,
Element,
Text,
CDATA,
Comment,
}
export type documentNode = {
type: NodeType.Document;
childNodes: serializedNodeWithId[];
};
export type documentTypeNode = {
type: NodeType.DocumentType;
name: string;
publicId: string;
systemId: string;
};
export type attributes = {
[key: string]: string;
};
export type elementNode = {
type: NodeType.Element;
tagName: string;
attributes: attributes;
childNodes: serializedNodeWithId[];
};
export type textNode = {
type: NodeType.Text;
textContent: string;
};
export type cdataNode = {
type: NodeType.CDATA;
textContent: '';
};
export type commentNode = {
type: NodeType.Comment;
textContent: string;
};
export type serializedNode =
| documentNode
| documentTypeNode
| elementNode
| textNode
| cdataNode
| commentNode;
export type serializedNodeWithId = serializedNode & { id: number };