@lobehub/editor 4.29.0 → 4.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/es/editor-kernel/index.d.ts +2 -2
- package/es/editor-kernel/index.js +2 -2
- package/es/headless/index.d.ts +5 -2
- package/es/headless/index.js +20 -22
- package/es/headless.d.ts +6 -507
- package/es/headless.js +906 -411
- package/es/i-node-identity-service-BBC_bg97.d.ts +522 -0
- package/es/index.d.ts +4 -2
- package/es/index.js +5 -2
- package/es/plugins/block/react/ReactBlockPlugin.js +0 -3
- package/es/plugins/common/data-source/json-data-source.js +33 -25
- package/es/plugins/common/data-source/text-data-source.js +6 -1
- package/es/plugins/common/index.d.ts +3 -1
- package/es/plugins/common/node/node-id.d.ts +7 -0
- package/es/plugins/common/node/node-id.js +484 -0
- package/es/plugins/common/node/node-identity-policy.d.ts +26 -0
- package/es/plugins/common/node/node-identity-policy.js +68 -0
- package/es/plugins/common/plugin/index.js +28 -2
- package/es/plugins/common/service/i-node-identity-service.d.ts +19 -0
- package/es/plugins/common/service/i-node-identity-service.js +4 -0
- package/es/plugins/common/service/node-identity-service.js +87 -0
- package/es/plugins/common/utils/index.js +3 -1
- package/es/plugins/list/plugin/index.js +7 -1
- package/es/plugins/litexml/command/index.d.ts +2 -2
- package/es/plugins/litexml/command/index.js +464 -292
- package/es/plugins/litexml/command/symbols.d.ts +18 -7
- package/es/plugins/litexml/command/symbols.js +2 -1
- package/es/plugins/litexml/data-source/litexml-data-source.js +35 -32
- package/es/plugins/litexml/diff-validation.js +68 -14
- package/es/plugins/litexml/index.d.ts +2 -2
- package/es/plugins/litexml/node/identity-policy.js +91 -0
- package/es/plugins/litexml/plugin/index.js +6 -0
- package/es/plugins/litexml/table-cell-diff.js +4 -0
- package/es/plugins/litexml/table-row-diff.js +4 -0
- package/es/plugins/litexml/utils/index.js +10 -23
- package/es/plugins/markdown/command/index.js +77 -4
- package/es/plugins/markdown/data-source/markdown-data-source.js +6 -1
- package/es/plugins/math/plugin/index.js +2 -2
- package/es/plugins/slash/service/i-slash-service.d.ts +2 -2
- package/es/plugins/yjs/plugin/index.js +5 -3
- package/es/plugins/yjs/plugin/utils/editor-state.js +8 -1
- package/es/plugins/yjs/plugin/utils/node-identity.js +106 -0
- package/es/plugins/yjs/service/index.js +38 -4
- package/es/{symbols-Do7jD4Tt.d.ts → symbols-Dyr9yyS0.d.ts} +18 -7
- package/package.json +4 -4
- package/scripts/postinstall-lexical-patch.cjs +0 -21
- package/patches/lexical@0.42.0.patch +0 -82
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $normalizeNodeIds, init_node_id, migrateSerializedNodeIds } from "../node/node-id.js";
|
|
2
2
|
import { exportNodeToJSON, init_utils } from "../utils/index.js";
|
|
3
3
|
import DataSource from "../../../editor-kernel/data-source.js";
|
|
4
4
|
import { cursorNodeSerialized } from "../node/cursor.js";
|
|
5
|
-
import { $parseSerializedNodeImpl } from "../../litexml/utils/index.js";
|
|
6
5
|
import { $isTableSelection } from "@lexical/table";
|
|
7
|
-
import { $getCharacterOffsets, $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, IS_CODE
|
|
6
|
+
import { $getCharacterOffsets, $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, IS_CODE } from "lexical";
|
|
8
7
|
//#region src/plugins/common/data-source/json-data-source.ts
|
|
9
|
-
|
|
8
|
+
init_node_id();
|
|
10
9
|
init_utils();
|
|
11
10
|
var JSONDataSource = class extends DataSource {
|
|
12
11
|
read(editor, data, options = {}) {
|
|
13
|
-
let
|
|
14
|
-
if (typeof data === "string")
|
|
15
|
-
else
|
|
12
|
+
let inputData;
|
|
13
|
+
if (typeof data === "string") inputData = JSON.parse(data);
|
|
14
|
+
else inputData = data;
|
|
15
|
+
const dataObj = structuredClone(inputData);
|
|
16
|
+
if (!(options.keepId ?? dataObj.keepId ?? false)) {
|
|
17
|
+
const stripNodeIds = (node) => {
|
|
18
|
+
delete node.id;
|
|
19
|
+
const state = node.$;
|
|
20
|
+
if (state && typeof state === "object" && state.properties) {
|
|
21
|
+
const properties = { ...state.properties };
|
|
22
|
+
delete properties.nodeId;
|
|
23
|
+
if (Object.keys(properties).length > 0) node.$ = {
|
|
24
|
+
...state,
|
|
25
|
+
properties
|
|
26
|
+
};
|
|
27
|
+
else {
|
|
28
|
+
const nextState = { ...state };
|
|
29
|
+
delete nextState.properties;
|
|
30
|
+
if (Object.keys(nextState).length > 0) node.$ = nextState;
|
|
31
|
+
else delete node.$;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(node.children)) node.children.forEach((child) => stripNodeIds(child));
|
|
35
|
+
};
|
|
36
|
+
stripNodeIds(dataObj.root);
|
|
37
|
+
} else migrateSerializedNodeIds(dataObj.root);
|
|
16
38
|
const process = (node) => {
|
|
17
39
|
for (let i = 0; i < node.children.length; i++) {
|
|
18
40
|
const child = node.children[i];
|
|
@@ -34,24 +56,10 @@ var JSONDataSource = class extends DataSource {
|
|
|
34
56
|
}
|
|
35
57
|
};
|
|
36
58
|
process(dataObj.root);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let maxId = -1;
|
|
42
|
-
Array.from(state._nodeMap.keys()).forEach((key) => {
|
|
43
|
-
if (key === "root") return;
|
|
44
|
-
const numericKey = Number(key);
|
|
45
|
-
if (Number.isInteger(numericKey) && numericKey >= 0) maxId = Math.max(maxId, numericKey);
|
|
46
|
-
});
|
|
47
|
-
resetRandomKey(maxId + 1);
|
|
48
|
-
state._nodeMap.set(root.getKey(), root);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error(error);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
editor.setEditorState(state);
|
|
54
|
-
} else editor.setEditorState(editor.parseEditorState({ root: dataObj.root }));
|
|
59
|
+
const editorState = editor.parseEditorState({ root: dataObj.root }, () => {
|
|
60
|
+
$normalizeNodeIds($getRoot());
|
|
61
|
+
});
|
|
62
|
+
editor.setEditorState(editorState);
|
|
55
63
|
}
|
|
56
64
|
write(editor, options) {
|
|
57
65
|
if (options?.selection) return editor.getEditorState().read(() => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { INodeHelper, init_helper } from "../../../editor-kernel/inode/helper.js";
|
|
2
|
+
import { $normalizeNodeIds, init_node_id } from "../node/node-id.js";
|
|
2
3
|
import DataSource from "../../../editor-kernel/data-source.js";
|
|
3
4
|
import { $getRoot, $getSelection } from "lexical";
|
|
4
5
|
//#region src/plugins/common/data-source/text-data-source.ts
|
|
5
6
|
init_helper();
|
|
7
|
+
init_node_id();
|
|
6
8
|
var TextDataSource = class extends DataSource {
|
|
7
9
|
read(editor, data) {
|
|
8
10
|
if (typeof data !== "string") throw new Error("Invalid data type " + typeof data);
|
|
@@ -16,7 +18,10 @@ var TextDataSource = class extends DataSource {
|
|
|
16
18
|
}
|
|
17
19
|
INodeHelper.appendChild(rootNode, paragraph);
|
|
18
20
|
});
|
|
19
|
-
editor.
|
|
21
|
+
const editorState = editor.parseEditorState({ root: rootNode }, () => {
|
|
22
|
+
$normalizeNodeIds($getRoot());
|
|
23
|
+
});
|
|
24
|
+
editor.setEditorState(editorState);
|
|
20
25
|
}
|
|
21
26
|
write(editor, options) {
|
|
22
27
|
if (options?.selection) return editor.getEditorState().read(() => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { $getNodeById, $getNodeId } from "./node/node-id.js";
|
|
2
|
+
import { INodeIdentityService, NodeIdentitySnapshot } from "./service/i-node-identity-service.js";
|
|
1
3
|
import { INSERT_HEADING_COMMAND, INSERT_QUOTE_COMMAND } from "./command/index.js";
|
|
2
4
|
import { $createCursorNode, $isCardLikeElementNode, $isCursorNode, CardLikeElementNode, cursorNodeSerialized } from "./node/cursor.js";
|
|
3
5
|
import { CommonPlugin, CommonPluginOptions } from "./plugin/index.js";
|
|
@@ -5,4 +7,4 @@ import { ReactEditorContentProps, ReactPlainTextProps } from "./react/type.js";
|
|
|
5
7
|
import ReactEditorContent from "./react/ReactEditorContent.js";
|
|
6
8
|
import ReactPlainText from "./react/ReactPlainText.js";
|
|
7
9
|
import "./react/index.js";
|
|
8
|
-
export { $createCursorNode, $isCardLikeElementNode, $isCursorNode, CardLikeElementNode, CommonPlugin, CommonPluginOptions, INSERT_HEADING_COMMAND, INSERT_QUOTE_COMMAND, ReactEditorContent, ReactEditorContentProps, ReactPlainText, ReactPlainTextProps, cursorNodeSerialized };
|
|
10
|
+
export { $createCursorNode, $getNodeById, $getNodeId, $isCardLikeElementNode, $isCursorNode, CardLikeElementNode, CommonPlugin, CommonPluginOptions, INSERT_HEADING_COMMAND, INSERT_QUOTE_COMMAND, INodeIdentityService, type NodeIdentitySnapshot, ReactEditorContent, ReactEditorContentProps, ReactPlainText, ReactPlainTextProps, cursorNodeSerialized };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EditorState, LexicalEditor, LexicalNode } from "lexical";
|
|
2
|
+
//#region src/plugins/common/node/node-id.d.ts
|
|
3
|
+
declare function $getNodeId(node: LexicalNode): string | undefined;
|
|
4
|
+
/** Find the active representation of a logical ID in a Lexical read/update context. */
|
|
5
|
+
declare function $getNodeById(nodeId: string, root?: LexicalNode): LexicalNode | null;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { $getNodeById, $getNodeId };
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { __esmMin } from "../../../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { $getNodeIdentityPolicy, init_node_identity_policy } from "./node-identity-policy.js";
|
|
3
|
+
import { $getEditor, $getRoot, $getState, $isElementNode, $isRootNode, $setState, createState } from "lexical";
|
|
4
|
+
//#region src/plugins/common/node/node-id.ts
|
|
5
|
+
/** `root` is the document insertion anchor, never a content-node identity. */
|
|
6
|
+
function isValidContentNodeId(value) {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "root";
|
|
8
|
+
}
|
|
9
|
+
function $getNodeId(node) {
|
|
10
|
+
const nodeId = $getState(node, nodePropertiesState).nodeId;
|
|
11
|
+
return isValidContentNodeId(nodeId) ? nodeId : void 0;
|
|
12
|
+
}
|
|
13
|
+
function $setNodeId(node, nodeId) {
|
|
14
|
+
const normalized = nodeId.trim();
|
|
15
|
+
if (!normalized) throw new Error("Node ID must be a non-empty string.");
|
|
16
|
+
if (!isValidContentNodeId(normalized)) throw new Error("Node ID \"root\" is reserved for the document anchor.");
|
|
17
|
+
$setState(node, nodePropertiesState, (properties) => ({
|
|
18
|
+
...properties,
|
|
19
|
+
nodeId: normalized
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
function $clearNodeId(node) {
|
|
23
|
+
$setState(node, nodePropertiesState, (properties) => {
|
|
24
|
+
const next = { ...properties };
|
|
25
|
+
delete next.nodeId;
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function $copyNodeProperties(source, target) {
|
|
30
|
+
$setState(target, nodePropertiesState, parseProperties($getState(source, nodePropertiesState)));
|
|
31
|
+
}
|
|
32
|
+
/** Migrate explicit IDs from the legacy serialized `id` field into NodeState. */
|
|
33
|
+
function migrateSerializedNodeIds(node) {
|
|
34
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
35
|
+
const record = node;
|
|
36
|
+
const legacyId = typeof record.id === "string" || typeof record.id === "number" ? String(record.id).trim() : "";
|
|
37
|
+
if (record.type !== "root") {
|
|
38
|
+
const state = record.$ && typeof record.$ === "object" && !Array.isArray(record.$) ? record.$ : {};
|
|
39
|
+
const properties = state.properties && typeof state.properties === "object" && !Array.isArray(state.properties) ? state.properties : {};
|
|
40
|
+
if (!isValidContentNodeId(properties.nodeId) && isValidContentNodeId(legacyId)) record.$ = {
|
|
41
|
+
...state,
|
|
42
|
+
properties: {
|
|
43
|
+
...properties,
|
|
44
|
+
nodeId: legacyId
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
else if (!isValidContentNodeId(properties.nodeId) && "nodeId" in properties) {
|
|
48
|
+
const nextProperties = { ...properties };
|
|
49
|
+
delete nextProperties.nodeId;
|
|
50
|
+
const nextState = {
|
|
51
|
+
...state,
|
|
52
|
+
properties: nextProperties
|
|
53
|
+
};
|
|
54
|
+
if (Object.keys(nextProperties).length === 0) delete nextState.properties;
|
|
55
|
+
if (Object.keys(nextState).length === 0) delete record.$;
|
|
56
|
+
else record.$ = nextState;
|
|
57
|
+
}
|
|
58
|
+
if (!isValidContentNodeId(legacyId) && "id" in record) delete record.id;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(record.children)) record.children.forEach(migrateSerializedNodeIds);
|
|
61
|
+
}
|
|
62
|
+
function hasSameSerializedContent(left, right) {
|
|
63
|
+
if (Object.is(left, right)) return true;
|
|
64
|
+
if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => hasSameSerializedContent(item, right[index]));
|
|
65
|
+
if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
|
|
66
|
+
const leftRecord = left;
|
|
67
|
+
const rightRecord = right;
|
|
68
|
+
if (typeof leftRecord.type === "string" && typeof rightRecord.type === "string" && leftRecord.type !== rightRecord.type) return false;
|
|
69
|
+
const getComparableEntries = (record) => {
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const [key, value] of Object.entries(record)) {
|
|
72
|
+
if (key === "id" && typeof record.type === "string") continue;
|
|
73
|
+
if (key === "$" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
74
|
+
const state = { ...value };
|
|
75
|
+
if (state.properties) {
|
|
76
|
+
state.properties = { ...state.properties };
|
|
77
|
+
delete state.properties.nodeId;
|
|
78
|
+
if (Object.keys(state.properties).length === 0) delete state.properties;
|
|
79
|
+
}
|
|
80
|
+
if (Object.keys(state).length === 0) continue;
|
|
81
|
+
entries.push([key, state]);
|
|
82
|
+
} else entries.push([key, value]);
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
};
|
|
86
|
+
const leftEntries = getComparableEntries(leftRecord);
|
|
87
|
+
const rightEntries = getComparableEntries(rightRecord);
|
|
88
|
+
if (leftEntries.length !== rightEntries.length) return false;
|
|
89
|
+
const rightValues = new Map(rightEntries);
|
|
90
|
+
for (const [key, value] of leftEntries) {
|
|
91
|
+
if (!rightValues.has(key)) return false;
|
|
92
|
+
if (!hasSameSerializedContent(value, rightValues.get(key))) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Preserve IDs only when an id-less snapshot node still represents the same subtree. */
|
|
97
|
+
function inheritMissingSerializedNodeIds(source, target) {
|
|
98
|
+
if (!source || typeof source !== "object" || Array.isArray(source) || !target || typeof target !== "object" || Array.isArray(target)) return;
|
|
99
|
+
const sourceRecord = source;
|
|
100
|
+
const targetRecord = target;
|
|
101
|
+
if (sourceRecord.type !== targetRecord.type) return;
|
|
102
|
+
const sourceId = serializedNodeId(sourceRecord);
|
|
103
|
+
if (sourceRecord.type !== "root" && sourceId && !serializedNodeId(targetRecord) && hasSameSerializedContent(sourceRecord, targetRecord)) {
|
|
104
|
+
const state = targetRecord.$ && typeof targetRecord.$ === "object" && !Array.isArray(targetRecord.$) ? targetRecord.$ : {};
|
|
105
|
+
const properties = state.properties && typeof state.properties === "object" && !Array.isArray(state.properties) ? state.properties : {};
|
|
106
|
+
targetRecord.$ = {
|
|
107
|
+
...state,
|
|
108
|
+
properties: {
|
|
109
|
+
...properties,
|
|
110
|
+
nodeId: sourceId
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(sourceRecord.children) && Array.isArray(targetRecord.children)) {
|
|
115
|
+
const length = Math.min(sourceRecord.children.length, targetRecord.children.length);
|
|
116
|
+
for (let index = 0; index < length; index++) inheritMissingSerializedNodeIds(sourceRecord.children[index], targetRecord.children[index]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function createNodeId() {
|
|
120
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
121
|
+
const cryptoObject = globalThis.crypto;
|
|
122
|
+
const id = [];
|
|
123
|
+
if (typeof cryptoObject?.getRandomValues === "function") {
|
|
124
|
+
while (id.length < 10) {
|
|
125
|
+
const bytes = cryptoObject.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
126
|
+
for (const byte of bytes) {
|
|
127
|
+
if (byte >= 252) continue;
|
|
128
|
+
id.push(alphabet[byte % 36]);
|
|
129
|
+
if (id.length === 10) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return id.join("");
|
|
133
|
+
}
|
|
134
|
+
for (let index = 0; index < 10; index++) id.push(alphabet[Math.floor(Math.random() * 36)]);
|
|
135
|
+
return id.join("");
|
|
136
|
+
}
|
|
137
|
+
/** Serialize a subtree without identities so sibling order cannot stand in for identity. */
|
|
138
|
+
function stableStringify(value) {
|
|
139
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
140
|
+
if (value && typeof value === "object") {
|
|
141
|
+
const record = value;
|
|
142
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify(value) ?? "undefined";
|
|
145
|
+
}
|
|
146
|
+
function identityFreeNodeTree(node, cache) {
|
|
147
|
+
const cached = cache.get(node);
|
|
148
|
+
if (cached) return cached;
|
|
149
|
+
const serialized = { ...node.exportJSON() };
|
|
150
|
+
delete serialized.id;
|
|
151
|
+
if (serialized.$?.properties) {
|
|
152
|
+
serialized.$ = {
|
|
153
|
+
...serialized.$,
|
|
154
|
+
properties: { ...serialized.$.properties }
|
|
155
|
+
};
|
|
156
|
+
delete serialized.$.properties.nodeId;
|
|
157
|
+
if (Object.keys(serialized.$.properties).length === 0) delete serialized.$.properties;
|
|
158
|
+
if (Object.keys(serialized.$).length === 0) delete serialized.$;
|
|
159
|
+
}
|
|
160
|
+
if ($isElementNode(node)) serialized.children = node.getChildren().map((child) => identityFreeNodeTree(child, cache));
|
|
161
|
+
cache.set(node, serialized);
|
|
162
|
+
return serialized;
|
|
163
|
+
}
|
|
164
|
+
/** A stable, opaque repair ID for malformed external snapshots with duplicate IDs. */
|
|
165
|
+
function duplicateRepairHashes(sourceId, identity, occurrence) {
|
|
166
|
+
const input = `${sourceId}\u0000${identity}\u0000${occurrence}`;
|
|
167
|
+
const hashes = [
|
|
168
|
+
2166136261,
|
|
169
|
+
2654435769,
|
|
170
|
+
2246822507,
|
|
171
|
+
3266489909
|
|
172
|
+
];
|
|
173
|
+
for (let index = 0; index < input.length; index++) {
|
|
174
|
+
const code = input.charCodeAt(index);
|
|
175
|
+
for (let part = 0; part < hashes.length; part++) hashes[part] = Math.imul(hashes[part] ^ code + part, 16777619);
|
|
176
|
+
}
|
|
177
|
+
return hashes;
|
|
178
|
+
}
|
|
179
|
+
function duplicateRepairId(sourceId, identity, occurrence) {
|
|
180
|
+
const hashes = duplicateRepairHashes(sourceId, identity, occurrence);
|
|
181
|
+
const partLength = 5;
|
|
182
|
+
const partRange = 36 ** partLength;
|
|
183
|
+
return hashes.slice(0, 2).map((hash) => ((hash >>> 0) % partRange).toString(36).padStart(partLength, "0")).join("");
|
|
184
|
+
}
|
|
185
|
+
function legacyDuplicateRepairId(sourceId, fingerprint, occurrence) {
|
|
186
|
+
return `dup-${duplicateRepairHashes(sourceId, fingerprint, occurrence).map((hash) => (hash >>> 0).toString(16).padStart(8, "0")).join("")}`;
|
|
187
|
+
}
|
|
188
|
+
function recordActiveOwners(node, owners, policy) {
|
|
189
|
+
const projection = policy.project(node);
|
|
190
|
+
if (projection === "hidden") return;
|
|
191
|
+
if (projection === "content") {
|
|
192
|
+
const nodeId = $getNodeId(node);
|
|
193
|
+
if (nodeId && !owners.has(nodeId)) owners.set(nodeId, node.getKey());
|
|
194
|
+
}
|
|
195
|
+
if ($isElementNode(node)) {
|
|
196
|
+
if (typeof projection === "object") {
|
|
197
|
+
const selected = node.getChildAtIndex(projection.childIndex);
|
|
198
|
+
if (selected) recordActiveOwners(selected, owners, policy);
|
|
199
|
+
} else node.getChildren().forEach((child) => recordActiveOwners(child, owners, policy));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Assign identities once per root transform and repair copy/split collisions in one pass. */
|
|
203
|
+
function $normalizeNodeIds(root, options = {}) {
|
|
204
|
+
if (!$isRootNode(root)) return;
|
|
205
|
+
const editor = $getEditor();
|
|
206
|
+
const policy = $getNodeIdentityPolicy();
|
|
207
|
+
const previousOwners = /* @__PURE__ */ new Map();
|
|
208
|
+
const previousFingerprints = /* @__PURE__ */ new Map();
|
|
209
|
+
const previousIdsByFingerprint = /* @__PURE__ */ new Map();
|
|
210
|
+
const identityFreeTrees = /* @__PURE__ */ new WeakMap();
|
|
211
|
+
const fingerprintCache = /* @__PURE__ */ new WeakMap();
|
|
212
|
+
const fingerprint = (node) => {
|
|
213
|
+
const cached = fingerprintCache.get(node);
|
|
214
|
+
if (cached) return cached;
|
|
215
|
+
const signature = stableStringify(identityFreeNodeTree(node, identityFreeTrees));
|
|
216
|
+
fingerprintCache.set(node, signature);
|
|
217
|
+
return signature;
|
|
218
|
+
};
|
|
219
|
+
const nodes = [];
|
|
220
|
+
const visit = (node) => {
|
|
221
|
+
if (policy.isIdentityNode(node)) nodes.push(node);
|
|
222
|
+
if ($isElementNode(node)) node.getChildren().forEach(visit);
|
|
223
|
+
};
|
|
224
|
+
root.getChildren().forEach(visit);
|
|
225
|
+
const idGroups = /* @__PURE__ */ new Map();
|
|
226
|
+
const missingIds = [];
|
|
227
|
+
for (const node of nodes) {
|
|
228
|
+
const nodeId = $getNodeId(node);
|
|
229
|
+
if (!nodeId) {
|
|
230
|
+
missingIds.push(node);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const group = idGroups.get(nodeId) || [];
|
|
234
|
+
group.push(node);
|
|
235
|
+
idGroups.set(nodeId, group);
|
|
236
|
+
}
|
|
237
|
+
const duplicateIds = new Set([...idGroups].filter(([, group]) => group.length > 1 && !(group.length === 2 && policy.canShareId(group[0], group[1]))).map(([nodeId]) => nodeId));
|
|
238
|
+
if (!options.stableOwnershipKey) editor.getEditorState().read(() => {
|
|
239
|
+
recordActiveOwners($getRoot(), previousOwners, policy);
|
|
240
|
+
if (options.stableDuplicateRepair && duplicateIds.size > 0) {
|
|
241
|
+
const visit = (node) => {
|
|
242
|
+
if (policy.isIdentityNode(node)) {
|
|
243
|
+
const nodeId = $getNodeId(node);
|
|
244
|
+
if (nodeId) {
|
|
245
|
+
const signature = fingerprint(node);
|
|
246
|
+
if (duplicateIds.has(nodeId)) {
|
|
247
|
+
const signatures = previousFingerprints.get(nodeId) || /* @__PURE__ */ new Set();
|
|
248
|
+
signatures.add(signature);
|
|
249
|
+
previousFingerprints.set(nodeId, signatures);
|
|
250
|
+
}
|
|
251
|
+
const ids = previousIdsByFingerprint.get(signature) || /* @__PURE__ */ new Set();
|
|
252
|
+
ids.add(nodeId);
|
|
253
|
+
previousIdsByFingerprint.set(signature, ids);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if ($isElementNode(node)) node.getChildren().forEach(visit);
|
|
257
|
+
};
|
|
258
|
+
$getRoot().getChildren().forEach(visit);
|
|
259
|
+
}
|
|
260
|
+
}, { editor });
|
|
261
|
+
const usedIds = new Set(idGroups.keys());
|
|
262
|
+
const reportRepair = (node, nodeId) => {
|
|
263
|
+
const previousId = $getNodeId(node);
|
|
264
|
+
$setNodeId(node, nodeId);
|
|
265
|
+
options.onRepair?.(node, previousId, nodeId);
|
|
266
|
+
};
|
|
267
|
+
const assignFreshId = (node, duplicateSourceId, stableIdentity) => {
|
|
268
|
+
let nodeId;
|
|
269
|
+
if (options.stableDuplicateRepair && duplicateSourceId !== void 0) {
|
|
270
|
+
const identity = stableIdentity ?? fingerprint(node);
|
|
271
|
+
const previousIds = stableIdentity ? void 0 : previousIdsByFingerprint.get(identity);
|
|
272
|
+
const reusableLegacyId = previousIds ? Array.from({ length: previousIds.size + 1 }, (_, index) => legacyDuplicateRepairId(duplicateSourceId, identity, index)).find((candidate) => candidate !== duplicateSourceId && previousIds.has(candidate) && !usedIds.has(candidate)) : void 0;
|
|
273
|
+
let occurrence = 0;
|
|
274
|
+
if (reusableLegacyId) nodeId = reusableLegacyId;
|
|
275
|
+
else do
|
|
276
|
+
nodeId = duplicateRepairId(duplicateSourceId, identity, occurrence++);
|
|
277
|
+
while (usedIds.has(nodeId));
|
|
278
|
+
} else {
|
|
279
|
+
nodeId = createNodeId();
|
|
280
|
+
while (usedIds.has(nodeId)) nodeId = createNodeId();
|
|
281
|
+
}
|
|
282
|
+
reportRepair(node, nodeId);
|
|
283
|
+
usedIds.add(nodeId);
|
|
284
|
+
};
|
|
285
|
+
const assignStableRepairGroup = (nodes, sourceId, identity) => {
|
|
286
|
+
let nodeId;
|
|
287
|
+
let occurrence = 0;
|
|
288
|
+
do
|
|
289
|
+
nodeId = duplicateRepairId(sourceId, identity, occurrence++);
|
|
290
|
+
while (usedIds.has(nodeId));
|
|
291
|
+
nodes.forEach((node) => reportRepair(node, nodeId));
|
|
292
|
+
usedIds.add(nodeId);
|
|
293
|
+
};
|
|
294
|
+
const duplicateGroups = [...idGroups].filter(([nodeId]) => duplicateIds.has(nodeId));
|
|
295
|
+
if (options.stableOwnershipKey) duplicateGroups.sort(([left], [right]) => compareStrings(left, right));
|
|
296
|
+
for (const [nodeId, group] of duplicateGroups) {
|
|
297
|
+
if (group.length < 2) continue;
|
|
298
|
+
if (options.stableOwnershipKey) {
|
|
299
|
+
const identities = group.map((node) => [node, options.stableOwnershipKey(node)]);
|
|
300
|
+
if (identities.some(([, identity]) => !identity)) continue;
|
|
301
|
+
const identityKeys = identities.map(([, identity]) => identity);
|
|
302
|
+
if (new Set(identityKeys).size !== identityKeys.length) continue;
|
|
303
|
+
const identityByNode = new Map(identities);
|
|
304
|
+
const reviewPairs = [];
|
|
305
|
+
for (let leftIndex = 0; leftIndex < group.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < group.length; rightIndex++) if (policy.canShareId(group[leftIndex], group[rightIndex])) reviewPairs.push([group[leftIndex], group[rightIndex]]);
|
|
306
|
+
const preserved = /* @__PURE__ */ new Set();
|
|
307
|
+
const pairedNodes = /* @__PURE__ */ new Set();
|
|
308
|
+
const matchedPairs = [];
|
|
309
|
+
if (reviewPairs.length > 0) {
|
|
310
|
+
const sortedPairs = reviewPairs.map(([left, right]) => compareStrings(identityByNode.get(left), identityByNode.get(right)) <= 0 ? [left, right] : [right, left]).sort((left, right) => {
|
|
311
|
+
const leftKey = left.map((node) => identityByNode.get(node)).join("\0");
|
|
312
|
+
const rightKey = right.map((node) => identityByNode.get(node)).join("\0");
|
|
313
|
+
return compareStrings(leftKey, rightKey);
|
|
314
|
+
});
|
|
315
|
+
const matchedNodes = /* @__PURE__ */ new Set();
|
|
316
|
+
for (const [left, right] of sortedPairs) {
|
|
317
|
+
if (matchedNodes.has(left) || matchedNodes.has(right)) continue;
|
|
318
|
+
matchedNodes.add(left);
|
|
319
|
+
matchedNodes.add(right);
|
|
320
|
+
pairedNodes.add(left);
|
|
321
|
+
pairedNodes.add(right);
|
|
322
|
+
matchedPairs.push([left, right]);
|
|
323
|
+
}
|
|
324
|
+
const primaryPair = matchedPairs[0];
|
|
325
|
+
if (primaryPair) {
|
|
326
|
+
preserved.add(primaryPair[0]);
|
|
327
|
+
preserved.add(primaryPair[1]);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const primary = [...group].sort((left, right) => compareStrings(identityByNode.get(left), identityByNode.get(right)))[0];
|
|
331
|
+
preserved.add(primary);
|
|
332
|
+
}
|
|
333
|
+
const losingPairs = matchedPairs.slice(1);
|
|
334
|
+
const losingNodes = group.filter((node) => !preserved.has(node) && !pairedNodes.has(node)).sort((left, right) => compareStrings(identityByNode.get(left), identityByNode.get(right)));
|
|
335
|
+
const repairGroups = [...losingPairs.map(([left, right]) => [left, right]), ...losingNodes.map((node) => [node])].sort((left, right) => compareStrings(left.map((node) => identityByNode.get(node)).sort(compareStrings).join("\0"), right.map((node) => identityByNode.get(node)).sort(compareStrings).join("\0")));
|
|
336
|
+
for (const repairGroup of repairGroups) assignStableRepairGroup(repairGroup, nodeId, repairGroup.map((node) => identityByNode.get(node)).sort(compareStrings).join("\0"));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const previousOwner = group.find((node) => node.getKey() === previousOwners.get(nodeId));
|
|
340
|
+
const matchingPreviousOwner = options.stableDuplicateRepair ? group.find((node) => previousFingerprints.get(nodeId)?.has(fingerprint(node))) : void 0;
|
|
341
|
+
const notPreviouslyRepaired = options.stableDuplicateRepair ? group.find((node) => {
|
|
342
|
+
const signature = fingerprint(node);
|
|
343
|
+
const priorIds = previousIdsByFingerprint.get(signature);
|
|
344
|
+
if (!priorIds) return true;
|
|
345
|
+
for (let occurrence = 0; occurrence <= priorIds.size; occurrence++) {
|
|
346
|
+
const repairId = duplicateRepairId(nodeId, signature, occurrence);
|
|
347
|
+
if (repairId !== nodeId && priorIds.has(repairId)) return false;
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}) : void 0;
|
|
351
|
+
const primary = previousOwner || matchingPreviousOwner || notPreviouslyRepaired || group[0];
|
|
352
|
+
const reviewCounterpart = group.find((node) => node !== primary && policy.canShareId(primary, node));
|
|
353
|
+
const preserved = /* @__PURE__ */ new Set([primary, ...reviewCounterpart ? [reviewCounterpart] : []]);
|
|
354
|
+
for (const node of group) if (!preserved.has(node)) assignFreshId(node, nodeId);
|
|
355
|
+
}
|
|
356
|
+
for (const node of missingIds) if (options.stableOwnershipKey && options.stableDuplicateRepair) {
|
|
357
|
+
const identity = options.stableOwnershipKey(node);
|
|
358
|
+
if (!identity) continue;
|
|
359
|
+
assignFreshId(node, "", identity);
|
|
360
|
+
} else assignFreshId(node);
|
|
361
|
+
}
|
|
362
|
+
function editorStateHasCompleteNodeIds(editorState, editor) {
|
|
363
|
+
let nodeCount = 0;
|
|
364
|
+
let complete = true;
|
|
365
|
+
editorState.read(() => {
|
|
366
|
+
const policy = $getNodeIdentityPolicy();
|
|
367
|
+
const visit = (node) => {
|
|
368
|
+
if (policy.isIdentityNode(node)) {
|
|
369
|
+
nodeCount += 1;
|
|
370
|
+
if (!$getNodeId(node)) complete = false;
|
|
371
|
+
}
|
|
372
|
+
if ($isElementNode(node)) node.getChildren().forEach(visit);
|
|
373
|
+
};
|
|
374
|
+
visit($getRoot());
|
|
375
|
+
}, { editor });
|
|
376
|
+
return nodeCount > 0 && complete;
|
|
377
|
+
}
|
|
378
|
+
/** Ensure newly imported nodes can be addressed before the enclosing update commits. */
|
|
379
|
+
function $ensureUniqueNodeIds(nodes) {
|
|
380
|
+
const policy = $getNodeIdentityPolicy();
|
|
381
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
382
|
+
const collectExisting = (node) => {
|
|
383
|
+
const nodeId = $getNodeId(node);
|
|
384
|
+
if (nodeId) usedIds.add(nodeId);
|
|
385
|
+
if ($isElementNode(node)) node.getChildren().forEach(collectExisting);
|
|
386
|
+
};
|
|
387
|
+
collectExisting($getRoot());
|
|
388
|
+
const ensureNode = (node) => {
|
|
389
|
+
if (!policy.isIdentityNode(node)) {
|
|
390
|
+
if ($isElementNode(node)) node.getChildren().forEach(ensureNode);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
let nodeId = $getNodeId(node);
|
|
394
|
+
if (!nodeId || usedIds.has(nodeId)) {
|
|
395
|
+
do
|
|
396
|
+
nodeId = createNodeId();
|
|
397
|
+
while (usedIds.has(nodeId));
|
|
398
|
+
$setNodeId(node, nodeId);
|
|
399
|
+
}
|
|
400
|
+
usedIds.add(nodeId);
|
|
401
|
+
if ($isElementNode(node)) node.getChildren().forEach(ensureNode);
|
|
402
|
+
};
|
|
403
|
+
nodes.forEach(ensureNode);
|
|
404
|
+
}
|
|
405
|
+
/** Find the active representation of a logical ID in a Lexical read/update context. */
|
|
406
|
+
function $getNodeById(nodeId, root) {
|
|
407
|
+
if (!isValidContentNodeId(nodeId)) return null;
|
|
408
|
+
const policy = $getNodeIdentityPolicy();
|
|
409
|
+
const start = root ?? $getRoot();
|
|
410
|
+
const visit = (node) => {
|
|
411
|
+
if (!$isElementNode(node)) return $getNodeId(node) === nodeId && policy.project(node) === "content" ? node : null;
|
|
412
|
+
const projection = policy.project(node);
|
|
413
|
+
if (projection === "hidden") return null;
|
|
414
|
+
if (projection === "content" && $getNodeId(node) === nodeId) return node;
|
|
415
|
+
if (typeof projection === "object") {
|
|
416
|
+
const selected = node.getChildAtIndex(projection.childIndex);
|
|
417
|
+
return selected ? visit(selected) : null;
|
|
418
|
+
}
|
|
419
|
+
for (const child of node.getChildren()) {
|
|
420
|
+
const found = visit(child);
|
|
421
|
+
if (found) return found;
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
};
|
|
425
|
+
return visit(start);
|
|
426
|
+
}
|
|
427
|
+
/** Build a key-only index for one immutable editor state; never cache LexicalNode objects. */
|
|
428
|
+
function $getActiveNodeIdKeys() {
|
|
429
|
+
const policy = $getNodeIdentityPolicy();
|
|
430
|
+
const keys = /* @__PURE__ */ new Map();
|
|
431
|
+
const visit = (node) => {
|
|
432
|
+
const projection = policy.project(node);
|
|
433
|
+
if (projection === "hidden") return;
|
|
434
|
+
if (projection === "content") {
|
|
435
|
+
const id = $getNodeId(node);
|
|
436
|
+
if (id && !keys.has(id)) keys.set(id, node.getKey());
|
|
437
|
+
}
|
|
438
|
+
if ($isElementNode(node)) {
|
|
439
|
+
if (typeof projection === "object") {
|
|
440
|
+
const selected = node.getChildAtIndex(projection.childIndex);
|
|
441
|
+
if (selected) visit(selected);
|
|
442
|
+
} else node.getChildren().forEach(visit);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
visit($getRoot());
|
|
446
|
+
return keys;
|
|
447
|
+
}
|
|
448
|
+
var cloneProperties, parseProperties, isEqual, compareStrings, nodePropertiesState, serializedNodeId, $findNodeById;
|
|
449
|
+
var init_node_id = __esmMin((() => {
|
|
450
|
+
init_node_identity_policy();
|
|
451
|
+
cloneProperties = (value) => {
|
|
452
|
+
if (typeof structuredClone === "function") try {
|
|
453
|
+
return structuredClone(value);
|
|
454
|
+
} catch {}
|
|
455
|
+
return JSON.parse(JSON.stringify(value));
|
|
456
|
+
};
|
|
457
|
+
parseProperties = (value) => {
|
|
458
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
459
|
+
const properties = cloneProperties(value);
|
|
460
|
+
const nodeId = properties.nodeId;
|
|
461
|
+
if (isValidContentNodeId(nodeId)) properties.nodeId = nodeId.trim();
|
|
462
|
+
else delete properties.nodeId;
|
|
463
|
+
return properties;
|
|
464
|
+
};
|
|
465
|
+
isEqual = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
466
|
+
compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
467
|
+
nodePropertiesState = createState("properties", {
|
|
468
|
+
isEqual,
|
|
469
|
+
unparse: cloneProperties,
|
|
470
|
+
parse: parseProperties
|
|
471
|
+
});
|
|
472
|
+
serializedNodeId = (node) => {
|
|
473
|
+
const nodeId = node.$?.properties?.nodeId;
|
|
474
|
+
if (isValidContentNodeId(nodeId)) return nodeId.trim();
|
|
475
|
+
if (typeof node.id === "string" || typeof node.id === "number") {
|
|
476
|
+
const legacyId = String(node.id).trim();
|
|
477
|
+
return isValidContentNodeId(legacyId) ? legacyId : void 0;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
$findNodeById = $getNodeById;
|
|
481
|
+
}));
|
|
482
|
+
//#endregion
|
|
483
|
+
init_node_id();
|
|
484
|
+
export { $clearNodeId, $copyNodeProperties, $ensureUniqueNodeIds, $findNodeById, $getActiveNodeIdKeys, $getNodeById, $getNodeId, $normalizeNodeIds, $setNodeId, editorStateHasCompleteNodeIds, inheritMissingSerializedNodeIds, init_node_id, isValidContentNodeId, migrateSerializedNodeIds, nodePropertiesState };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LexicalEditor, LexicalNode } from "lexical";
|
|
2
|
+
//#region src/plugins/common/node/node-identity-policy.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A document format can decide which physical nodes represent one logical ID.
|
|
5
|
+
* The common identity layer does not interpret review or document node types.
|
|
6
|
+
*/
|
|
7
|
+
type NodeIdentityProjection = 'container' | 'content' | 'hidden' | Readonly<{
|
|
8
|
+
childIndex: number;
|
|
9
|
+
}>;
|
|
10
|
+
interface NodeIdentityPolicy {
|
|
11
|
+
canShareId(left: LexicalNode, right: LexicalNode): boolean;
|
|
12
|
+
/** Classify one node for active lookup; hidden nodes still receive IDs in normalization. */
|
|
13
|
+
project(node: LexicalNode): NodeIdentityProjection;
|
|
14
|
+
isIdentityNode(node: LexicalNode): boolean;
|
|
15
|
+
}
|
|
16
|
+
declare function subscribeNodeIdentityPolicy(editor: LexicalEditor, listener: () => void): () => void;
|
|
17
|
+
declare function getNodeIdentityPolicyRevision(editor: LexicalEditor): number;
|
|
18
|
+
/** Register a document-format policy for one editor, with order-independent disposal. */
|
|
19
|
+
declare function registerNodeIdentityPolicy(editor: LexicalEditor, policy: NodeIdentityPolicy): () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Requires an editor-bound Lexical context. For an EditorState snapshot, pass
|
|
22
|
+
* `{ editor }` to `state.read` so its document policy is unambiguous.
|
|
23
|
+
*/
|
|
24
|
+
declare function $getNodeIdentityPolicy(): NodeIdentityPolicy;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { $getNodeIdentityPolicy, NodeIdentityPolicy, NodeIdentityProjection, getNodeIdentityPolicyRevision, registerNodeIdentityPolicy, subscribeNodeIdentityPolicy };
|