From 97c4b4f6e1519fc2e4b155c0b98cf3bbb5988f69 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] basic snapshot implementation --- src/index.ts | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 5 +- tslint.json | 3 +- 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index e69de29b..4a003d2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,138 @@ +let _id = 1; + +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; diff --git a/tsconfig.json b/tsconfig.json index 1aec14ff..ce1b7e3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "preserveConstEnums": true, "sourceMap": true, "rootDir": "src", - "outDir": "build" + "outDir": "build", + "lib": ["es6", "dom"] }, "compileOnSave": true -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index c82ebf60..3a0c0801 100644 --- a/tslint.json +++ b/tslint.json @@ -8,7 +8,8 @@ "ordered-imports": false, "object-literal-sort-keys": false, "no-unused-variable": true, - "object-literal-key-quotes": false + "object-literal-key-quotes": false, + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] }, "rulesDirectory": [] } \ No newline at end of file