@magic-marker/prosemirror-suggest-changes 0.3.2 → 0.3.3-wrap-unwrap.1

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.
Files changed (40) hide show
  1. package/dist/__tests__/playwrightPage.d.ts +8 -1
  2. package/dist/commands.js +132 -41
  3. package/dist/ensureSelectionPlugin.js +3 -2
  4. package/dist/features/wrapUnwrap/__tests__/listStructure.playwright.test.d.ts +1 -0
  5. package/dist/features/wrapUnwrap/addIdAttr.d.ts +2 -0
  6. package/dist/features/wrapUnwrap/addIdAttr.js +62 -0
  7. package/dist/features/wrapUnwrap/buildMaterializedPaths.d.ts +3 -0
  8. package/dist/features/wrapUnwrap/buildMaterializedPaths.js +71 -0
  9. package/dist/features/wrapUnwrap/constants.d.ts +3 -0
  10. package/dist/features/wrapUnwrap/constants.js +7 -0
  11. package/dist/features/wrapUnwrap/generateNodeId.d.ts +2 -0
  12. package/dist/features/wrapUnwrap/generateNodeId.js +6 -0
  13. package/dist/features/wrapUnwrap/getNodeId.d.ts +2 -0
  14. package/dist/features/wrapUnwrap/getNodeId.js +5 -0
  15. package/dist/features/wrapUnwrap/revertStructureSuggestions.d.ts +9 -0
  16. package/dist/features/wrapUnwrap/revertStructureSuggestions.js +433 -0
  17. package/dist/features/wrapUnwrap/sameParentChain.d.ts +2 -0
  18. package/dist/features/wrapUnwrap/sameParentChain.js +4 -0
  19. package/dist/features/wrapUnwrap/stableNodeIdsPlugin.d.ts +12 -0
  20. package/dist/features/wrapUnwrap/stableNodeIdsPlugin.js +95 -0
  21. package/dist/features/wrapUnwrap/structureChangesPlugin.d.ts +7 -0
  22. package/dist/features/wrapUnwrap/structureChangesPlugin.js +174 -0
  23. package/dist/features/wrapUnwrap/types.d.ts +45 -0
  24. package/dist/features/wrapUnwrap/types.js +13 -0
  25. package/dist/generateId.js +2 -2
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +3 -1
  28. package/dist/rebaseStep.d.ts +9 -0
  29. package/dist/rebaseStep.js +11 -0
  30. package/dist/replaceStep.d.ts +1 -1
  31. package/dist/schema.d.ts +2 -1
  32. package/dist/schema.js +37 -1
  33. package/dist/testing/e2eTestSchema.d.ts +2 -0
  34. package/dist/testing/testBuilders.d.ts +1 -2
  35. package/dist/utils.d.ts +1 -0
  36. package/dist/utils.js +6 -2
  37. package/dist/withSuggestChanges.d.ts +5 -1
  38. package/dist/withSuggestChanges.js +39 -2
  39. package/package.json +2 -2
  40. package/src/features/wrapUnwrap/README.md +127 -0
@@ -0,0 +1,433 @@
1
+ import { getSuggestionMarks } from "../../utils.js";
2
+ import { getNodeId } from "./getNodeId.js";
3
+ import { Transform } from "prosemirror-transform";
4
+ import { guardDocParent, guardDocWithChildren, guardStructureMarkAttrs } from "./types.js";
5
+ import { sameParentChain } from "./sameParentChain.js";
6
+ import { buildMaterializedPaths } from "./buildMaterializedPaths.js";
7
+ /* public */ export function applyAllStructureSuggestions(node) {
8
+ const tr = new Transform(node);
9
+ applyAllStructureMarks(tr);
10
+ return tr;
11
+ }
12
+ export function revertAllStructureSuggestions(node) {
13
+ const tr = new Transform(node);
14
+ revertAllStructureMarks(tr);
15
+ return tr;
16
+ }
17
+ export function applyOneStructureSuggestion(node, suggestionId) {
18
+ const tr = new Transform(node);
19
+ applyStructureMarkGroup(tr, suggestionId);
20
+ return tr;
21
+ }
22
+ export function revertOneStructureSuggestion(node, suggestionId) {
23
+ const tr = new Transform(node);
24
+ revertStructureMarkGroupInOrder(tr, suggestionId);
25
+ return tr;
26
+ }
27
+ export function applyAllStructureSuggestionsOnNode(node, from, to) {
28
+ const tr = new Transform(node);
29
+ applyAllStructureMarksOnNode(tr, node, from, to);
30
+ return tr;
31
+ }
32
+ export function revertAllStructureSuggestionsOnNode(node, from, to) {
33
+ const tr = new Transform(node);
34
+ revertAllStructureMarksOnNode(tr, node, from, to);
35
+ return tr;
36
+ }
37
+ /* private */ function applyAllStructureMarks(tr) {
38
+ applyAllStructureMarksOnNode(tr, tr.doc);
39
+ }
40
+ function revertAllStructureMarks(tr) {
41
+ revertAllStructureMarksOnNode(tr, tr.doc);
42
+ }
43
+ function applyAllStructureMarksOnNode(tr, node, from, to) {
44
+ const { structure } = getSuggestionMarks(node.type.schema);
45
+ const suggestionIds = new Set();
46
+ node.descendants((node, pos)=>{
47
+ if (from !== undefined && pos < from) {
48
+ return true;
49
+ }
50
+ if (to !== undefined && pos > to) {
51
+ return false;
52
+ }
53
+ if (node.isText) return true;
54
+ if (!structure.isInSet(node.marks)) return true;
55
+ node.marks.forEach((mark)=>{
56
+ if (mark.type !== structure) return;
57
+ const suggestionId = mark.attrs["id"];
58
+ suggestionIds.add(suggestionId);
59
+ });
60
+ return true;
61
+ });
62
+ for (const suggestionId of suggestionIds){
63
+ applyStructureMarkGroup(tr, suggestionId);
64
+ }
65
+ }
66
+ function revertAllStructureMarksOnNode(tr, node, from, to) {
67
+ const { structure } = getSuggestionMarks(node.type.schema);
68
+ // collect all structure mark ids
69
+ const suggestionIds = new Set();
70
+ node.descendants((node, pos)=>{
71
+ if (from !== undefined && pos < from) {
72
+ return true;
73
+ }
74
+ if (to !== undefined && pos > to) {
75
+ return false;
76
+ }
77
+ if (node.isText) return true;
78
+ if (!structure.isInSet(node.marks)) return true;
79
+ node.marks.forEach((mark)=>{
80
+ if (mark.type !== structure) return;
81
+ const suggestionId = mark.attrs["id"];
82
+ suggestionIds.add(suggestionId);
83
+ });
84
+ return true;
85
+ });
86
+ for (const suggestionId of suggestionIds){
87
+ revertStructureMarkGroupInOrder(tr, suggestionId);
88
+ }
89
+ }
90
+ function applyStructureMarkGroup(tr, suggestionId) {
91
+ const { structure } = getSuggestionMarks(tr.doc.type.schema);
92
+ tr.doc.descendants((node, pos)=>{
93
+ if (node.isText) return true;
94
+ if (!structure.isInSet(node.marks)) return true;
95
+ node.marks.forEach((mark)=>{
96
+ if (mark.type !== structure) return;
97
+ if (mark.attrs["id"] !== suggestionId) return;
98
+ applyStructureMark(tr, mark, pos);
99
+ });
100
+ return true;
101
+ });
102
+ }
103
+ function revertStructureMarkGroupInOrder(tr, suggestionId) {
104
+ console.group("reverting structure mark group", suggestionId);
105
+ const suggestionIds = findOrderedSuggestionIdsToRevert(tr.doc, suggestionId, buildMaterializedPaths(tr.doc));
106
+ console.log("suggestion groups to revert", suggestionIds);
107
+ for (const suggestionId of suggestionIds){
108
+ revertStructureMarkGroup(tr, suggestionId);
109
+ }
110
+ console.groupEnd();
111
+ }
112
+ function revertStructureMarkGroup(tr, suggestionId) {
113
+ console.group("reverting structure suggestion", suggestionId);
114
+ let structureMark = findNextStructureMark(tr.doc, suggestionId);
115
+ while(structureMark !== null){
116
+ console.groupCollapsed("reverting structure mark", structureMark.mark.attrs["id"], "at pos", structureMark.pos, "at node", structureMark.node.toString(), {
117
+ structureMark
118
+ });
119
+ revertStructureMark(tr, structureMark.mark, structureMark.pos);
120
+ console.groupEnd();
121
+ structureMark = findNextStructureMark(tr.doc, suggestionId);
122
+ }
123
+ console.groupEnd();
124
+ }
125
+ function applyStructureMark(tr, mark, pos) {
126
+ tr.removeNodeMark(pos, mark);
127
+ }
128
+ function revertStructureMark(tr, mark, pos) {
129
+ const transform = new Transform(tr.doc);
130
+ transform.removeNodeMark(pos, mark);
131
+ const node = transform.doc.nodeAt(pos);
132
+ if (!node) {
133
+ throw new Error(`Node not found at position ${String(pos)}`);
134
+ }
135
+ console.log("revertStructureMark", mark.attrs["id"], "at node", node.toString(), {
136
+ node,
137
+ mark,
138
+ pos
139
+ });
140
+ const attrs = mark.attrs;
141
+ if (!guardStructureMarkAttrs(attrs)) {
142
+ console.warn("revertStructureMark", "invalid shape of structure mark attrs", {
143
+ attrs
144
+ });
145
+ return;
146
+ }
147
+ const op = attrs.data.op;
148
+ switch(op.op){
149
+ case "add":
150
+ {
151
+ revertAdd(op, transform, node, pos);
152
+ break;
153
+ }
154
+ case "move":
155
+ {
156
+ revertMove(op, transform, node, pos);
157
+ break;
158
+ }
159
+ default:
160
+ console.warn("revertStructureMark", "unknown op", {
161
+ op
162
+ });
163
+ break;
164
+ }
165
+ transform.steps.forEach((step)=>{
166
+ tr.step(step);
167
+ });
168
+ }
169
+ function revertAdd(op, tr, node, pos) {
170
+ console.log("revertAdd", "node", node.toString(), "was added at", pos, {
171
+ op,
172
+ node,
173
+ pos
174
+ });
175
+ deleteNodeWithParents(tr, node, pos);
176
+ }
177
+ function revertMove(op, tr, node, pos) {
178
+ console.log("revertMove", "node", node.toString(), "was moved from", op.from, {
179
+ op,
180
+ node
181
+ });
182
+ const parent = getDeepestSurvivingParent(op.from, tr.doc);
183
+ console.log("revertMove", "deepest surviving parent is", parent.node.toString(), {
184
+ parent
185
+ });
186
+ const child = wrapNodeInParentChain(parent.remainingChain, node);
187
+ console.log("revertMove", "wrapped node in parent chain is", child.toString(), {
188
+ child
189
+ });
190
+ const insertionPos = findInsertionPos(parent.node, parent.pos, parent.parent);
191
+ console.log("revertMove", "insertion pos", {
192
+ insertionPos
193
+ });
194
+ tr.insert(insertionPos, child);
195
+ const mappedPos = tr.mapping.map(pos);
196
+ deleteNodeWithParents(tr, node, mappedPos);
197
+ }
198
+ // given a suggestion id
199
+ // return an array of suggestion ids to revert
200
+ // resolve suggestion dependencies, meaning,
201
+ // if to revert suggestion id 1 you need to revert 2, and to revert 2 you need to revert 3
202
+ // it will return [3,2,1]
203
+ // todo: this should probably use topological sort at some point
204
+ function findOrderedSuggestionIdsToRevert(node, suggestionId, materializedPaths) {
205
+ const { structure } = getSuggestionMarks(node.type.schema);
206
+ // collect marks with the given suggestionId
207
+ const markGroup = [];
208
+ node.descendants((descendant, pos)=>{
209
+ if (descendant.isText) return true;
210
+ if (!structure.isInSet(descendant.marks)) return true;
211
+ descendant.marks.forEach((mark)=>{
212
+ if (mark.type !== structure) return;
213
+ if (mark.attrs["id"] !== suggestionId) return;
214
+ markGroup.push({
215
+ mark,
216
+ node: descendant,
217
+ pos
218
+ });
219
+ });
220
+ return true;
221
+ });
222
+ const suggestionIds = new Set();
223
+ suggestionIds.add(suggestionId);
224
+ // find first mark that doesn't have a matching op.to
225
+ const mismatch = markGroup.find((mark)=>{
226
+ const nodeId = getNodeId(mark.node);
227
+ if (nodeId == null) return false;
228
+ const parentChain = materializedPaths.get(nodeId);
229
+ if (parentChain == null) return false;
230
+ const { attrs } = mark.mark;
231
+ if (!guardStructureMarkAttrs(attrs)) return false;
232
+ if (attrs.data.op.op !== "move") return false;
233
+ return !sameParentChain(attrs.data.op.to, parentChain.chain);
234
+ });
235
+ console.log("mismatch?", mismatch);
236
+ if (mismatch == null) {
237
+ return Array.from(suggestionIds).reverse();
238
+ }
239
+ // find first mark on the node that does have a matching op.to
240
+ const match = mismatch.node.marks.find((mark)=>{
241
+ if (mark.type !== structure) return false;
242
+ const { attrs } = mark;
243
+ if (!guardStructureMarkAttrs(attrs)) return false;
244
+ if (attrs.data.op.op !== "move") return false;
245
+ const nodeId = getNodeId(mismatch.node);
246
+ if (nodeId == null) return false;
247
+ const parentChain = materializedPaths.get(nodeId);
248
+ if (parentChain == null) return false;
249
+ return sameParentChain(attrs.data.op.to, parentChain.chain);
250
+ });
251
+ if (match) {
252
+ suggestionIds.add(match.attrs["id"]);
253
+ }
254
+ return Array.from(suggestionIds).reverse();
255
+ }
256
+ function findNextStructureMark(doc, suggestionId) {
257
+ console.log("findNextStructureMark", suggestionId);
258
+ const { structure } = getSuggestionMarks(doc.type.schema);
259
+ let structureMark = null;
260
+ doc.nodesBetween(0, doc.content.size, (node, pos)=>{
261
+ const mark = node.marks.find((mark)=>mark.type === structure && mark.attrs["id"] === suggestionId);
262
+ if (mark) {
263
+ structureMark = {
264
+ mark,
265
+ node,
266
+ pos
267
+ };
268
+ }
269
+ return structureMark === null;
270
+ });
271
+ if (structureMark) {
272
+ console.log("findStructureMark", "found structure mark with id", suggestionId, {
273
+ structureMark,
274
+ suggestionId
275
+ });
276
+ } else {
277
+ console.log("findStructureMark", "no structure mark found with id", suggestionId);
278
+ }
279
+ return structureMark;
280
+ }
281
+ function getNodesWithChildren(doc) {
282
+ const nodesWithChildren = new Map();
283
+ const docWithChildren = {
284
+ node: doc,
285
+ pos: null,
286
+ children: new Set()
287
+ };
288
+ doc.children.forEach((child)=>{
289
+ const nodeId = getNodeId(child);
290
+ if (nodeId == null) return;
291
+ docWithChildren.children.add(nodeId);
292
+ });
293
+ nodesWithChildren.set("__doc__", docWithChildren);
294
+ doc.descendants((node, pos)=>{
295
+ if (node.isText) return true;
296
+ const nodeId = getNodeId(node);
297
+ if (nodeId == null) return true;
298
+ if (nodesWithChildren.has(nodeId)) return true;
299
+ const children = node.children.reduce((acc, child)=>{
300
+ const childId = getNodeId(child);
301
+ if (childId == null) return acc;
302
+ acc.add(childId);
303
+ return acc;
304
+ }, new Set());
305
+ nodesWithChildren.set(nodeId, {
306
+ node,
307
+ pos,
308
+ children
309
+ });
310
+ return true;
311
+ });
312
+ return nodesWithChildren;
313
+ }
314
+ // given a chain of parent node descriptors, follow the chain from top to bottom as long as nodes exist
315
+ // return the deepest existing parent node descriptor, along with the actual node and the pos in the current document
316
+ // also return the remaining part of the chain
317
+ function getDeepestSurvivingParent(parentChain, doc) {
318
+ const chain = [
319
+ ...parentChain
320
+ ].reverse();
321
+ const currentNodes = getNodesWithChildren(doc);
322
+ let currentNode = currentNodes.get("__doc__"); // get doc node with children from current doc
323
+ if (!guardDocWithChildren(currentNode)) {
324
+ throw new Error("doc not found in nodesWithChildren");
325
+ }
326
+ let parent = chain.shift(); // get doc node descriptor from parent chain
327
+ if (!guardDocParent(parent)) {
328
+ throw new Error("doc parent not found in op chain");
329
+ }
330
+ let remainingChain = [];
331
+ for (const [index, nextParent] of chain.entries()){
332
+ const nextNodeWithChildren = currentNodes.get(nextParent.nodeId);
333
+ // nextParent does not exist in the current document at all
334
+ if (nextNodeWithChildren == null) {
335
+ remainingChain = chain.slice(index);
336
+ break;
337
+ }
338
+ // nextParent exists in the document, but in a different parent chain
339
+ if (!currentNode.children.has(nextParent.nodeId)) {
340
+ remainingChain = chain.slice(index);
341
+ break;
342
+ }
343
+ currentNode = nextNodeWithChildren;
344
+ parent = nextParent;
345
+ }
346
+ return {
347
+ parent,
348
+ node: currentNode.node,
349
+ pos: currentNode.pos,
350
+ remainingChain: [
351
+ ...remainingChain
352
+ ].reverse()
353
+ };
354
+ }
355
+ // given a chain of parent node descriptors and a node
356
+ // wrap the node in the parent chain
357
+ function wrapNodeInParentChain(parentChain, node) {
358
+ let child = node.copy(node.content);
359
+ for (const parent of parentChain){
360
+ const schema = node.type.schema;
361
+ const nodeType = schema.nodes[parent.nodeType];
362
+ if (!nodeType) {
363
+ throw new Error(`node type ${parent.nodeType} not found in schema`);
364
+ }
365
+ const marks = parent.nodeMarks.map((mark)=>schema.markFromJSON(mark));
366
+ child = nodeType.create(parent.nodeAttrs, child, marks);
367
+ }
368
+ return child;
369
+ }
370
+ // given a node, its position, and a parent descriptor of this node in some parent chain,
371
+ // use the info from the descriptor to find the insertion position in the node
372
+ // first try to find siblings, fallback to end of node
373
+ function findInsertionPos(node, pos, parent) {
374
+ let leftSibling = null;
375
+ let rightSibling = null;
376
+ node.descendants((child, localChildPos)=>{
377
+ const childId = getNodeId(child);
378
+ if (childId == null) return false;
379
+ const globalChildPos = pos != null ? pos + 1 + localChildPos : localChildPos;
380
+ if (parent.childSiblingIds[0] === childId) {
381
+ leftSibling = {
382
+ node: child,
383
+ pos: globalChildPos
384
+ };
385
+ }
386
+ if (parent.childSiblingIds[1] === childId) {
387
+ rightSibling = {
388
+ node: child,
389
+ pos: globalChildPos
390
+ };
391
+ }
392
+ // iterate only direct children
393
+ return false;
394
+ });
395
+ if (rightSibling != null) {
396
+ // insert before right sibling
397
+ return rightSibling.pos;
398
+ }
399
+ if (leftSibling != null) {
400
+ // insert after left sibling
401
+ return leftSibling.pos + leftSibling.node.nodeSize;
402
+ }
403
+ // insert at end of node
404
+ return pos != null ? pos + node.nodeSize - 1 : node.nodeSize - 1;
405
+ }
406
+ // delete a given node, and traverse upwards deleting parent nodes if they are now empty
407
+ function deleteNodeWithParents(transform, node, pos) {
408
+ let $mappedPos = transform.doc.resolve(pos);
409
+ let deleteFrom = $mappedPos.pos;
410
+ let deleteTo = $mappedPos.pos + node.nodeSize;
411
+ console.log("deleteNodeWithParents", "initial delete range covers node", node.toString(), {
412
+ deleteFrom,
413
+ deleteTo,
414
+ $mappedPos
415
+ });
416
+ while($mappedPos.depth > 0){
417
+ $mappedPos = transform.doc.resolve($mappedPos.before());
418
+ if ($mappedPos.nodeAfter?.childCount !== 1) break;
419
+ deleteFrom = $mappedPos.pos;
420
+ deleteTo = $mappedPos.pos + $mappedPos.nodeAfter.nodeSize;
421
+ console.log("deleteNodeWithParents", "expanded delete range to cover node", $mappedPos.nodeAfter.toString(), {
422
+ deleteFrom,
423
+ deleteTo,
424
+ $mappedPos
425
+ });
426
+ }
427
+ console.log("deleteNodeWithParents", "final delete range covers node", $mappedPos.nodeAfter?.toString(), {
428
+ deleteFrom,
429
+ deleteTo,
430
+ $mappedPos
431
+ });
432
+ transform.delete(deleteFrom, deleteTo);
433
+ }
@@ -0,0 +1,2 @@
1
+ import { type Parent } from "./types.js";
2
+ export declare function sameParentChain(parentChainA: Parent[], parentChainB: Parent[]): boolean;
@@ -0,0 +1,4 @@
1
+ export function sameParentChain(parentChainA, parentChainB) {
2
+ if (parentChainA.length !== parentChainB.length) return false;
3
+ return parentChainA.every((parent, index)=>parent.nodeId === parentChainB[index]?.nodeId);
4
+ }
@@ -0,0 +1,12 @@
1
+ import { type Node } from "prosemirror-model";
2
+ import { Plugin, PluginKey } from "prosemirror-state";
3
+ import { Transform } from "prosemirror-transform";
4
+ import { type NodeIdGenerator } from "./types.js";
5
+ export declare const stableNodeIdsKey: PluginKey<{
6
+ completedInitialRun: boolean;
7
+ }>;
8
+ export declare const STABLE_NODE_IDS_PLUGIN_META = "stable-node-ids-plugin";
9
+ export declare function stableNodeIds(generateNodeId: NodeIdGenerator): Plugin<{
10
+ completedInitialRun: boolean;
11
+ }>;
12
+ export declare function ensureStableIds(doc: Node, generateNodeId: NodeIdGenerator): Transform;
@@ -0,0 +1,95 @@
1
+ import { Plugin, PluginKey } from "prosemirror-state";
2
+ import { getNodeId } from "./getNodeId.js";
3
+ import { Transform } from "prosemirror-transform";
4
+ // stable ids plugin
5
+ // https://discuss.prosemirror.net/t/how-to-avoid-copying-attributes-to-new-paragraph/4568/2
6
+ // (also checks and fix duplicates that inevitably appear)
7
+ export const stableNodeIdsKey = new PluginKey("@handlewithcare/prosemirror-suggest-changes-stable-node-ids");
8
+ export const STABLE_NODE_IDS_PLUGIN_META = "stable-node-ids-plugin";
9
+ export function stableNodeIds(generateNodeId) {
10
+ return new Plugin({
11
+ key: stableNodeIdsKey,
12
+ appendTransaction (transactions, _oldState, newState) {
13
+ console.log("stableNodeIdsPlugin.appendTransaction");
14
+ const pluginState = stableNodeIdsKey.getState(newState);
15
+ // do nothing if doc hasn't changed (but make sure it runs initially)
16
+ const docChanged = transactions.some((transaction)=>transaction.docChanged);
17
+ if (!docChanged && pluginState?.completedInitialRun) {
18
+ console.warn("doc not changed, skipping stable node ids plugin", [
19
+ ...transactions
20
+ ]);
21
+ return;
22
+ }
23
+ console.groupCollapsed("stableNodeIdsPlugin", "appendTransaction");
24
+ console.log("stableNodeIdsPlugin", "appendTransaction", [
25
+ ...transactions
26
+ ]);
27
+ const tr = newState.tr;
28
+ const transform = ensureStableIds(tr.doc, generateNodeId);
29
+ transform.steps.forEach((step)=>{
30
+ tr.step(step);
31
+ });
32
+ console.log("tr steps", tr.steps);
33
+ console.groupEnd();
34
+ if (!tr.steps.length) return;
35
+ tr.setMeta(stableNodeIdsKey, STABLE_NODE_IDS_PLUGIN_META);
36
+ return tr;
37
+ },
38
+ state: {
39
+ init () {
40
+ return {
41
+ completedInitialRun: false
42
+ };
43
+ },
44
+ apply (tr, value) {
45
+ const meta = tr.getMeta(stableNodeIdsKey);
46
+ if (meta === STABLE_NODE_IDS_PLUGIN_META && !value.completedInitialRun) {
47
+ return {
48
+ completedInitialRun: true
49
+ };
50
+ }
51
+ return value;
52
+ }
53
+ }
54
+ });
55
+ }
56
+ export function ensureStableIds(doc, generateNodeId) {
57
+ const tr = new Transform(doc);
58
+ const nodeIds = new Set();
59
+ tr.doc.descendants((node, pos, parent, index)=>{
60
+ if (node.isText) return false;
61
+ const nodeId = getNodeId(node);
62
+ // nodeId is set and is not duplicated
63
+ if (nodeId != null && !nodeIds.has(nodeId)) {
64
+ nodeIds.add(nodeId);
65
+ return true;
66
+ }
67
+ // nodeId is set and it is duplicated
68
+ if (nodeId != null && nodeIds.has(nodeId)) {
69
+ const id = generateNodeId(node, pos, parent, index);
70
+ nodeIds.add(id);
71
+ tr.setNodeMarkup(pos, node.type, {
72
+ ...node.attrs,
73
+ id
74
+ }, node.marks);
75
+ console.log("fixed duplicate id", id, "for node", node.type.name, "at pos", pos, {
76
+ was: nodeId,
77
+ is: id
78
+ });
79
+ return true;
80
+ }
81
+ // node id is not set
82
+ if (nodeId == null) {
83
+ const id = generateNodeId(node, pos, parent, index);
84
+ nodeIds.add(id);
85
+ tr.setNodeMarkup(pos, node.type, {
86
+ ...node.attrs,
87
+ id
88
+ }, node.marks);
89
+ console.log("set stable id", id, "for node", node.type.name, "at pos", pos);
90
+ return true;
91
+ }
92
+ return true;
93
+ });
94
+ return tr;
95
+ }
@@ -0,0 +1,7 @@
1
+ import { type Schema, type Node } from "prosemirror-model";
2
+ import { Plugin, PluginKey } from "prosemirror-state";
3
+ import { type SuggestionId } from "../../generateId.js";
4
+ import { Transform } from "prosemirror-transform";
5
+ export declare const structureChangesKey: PluginKey<any>;
6
+ export declare function structureChangesPlugin(generateId?: (schema: Schema, doc?: Node) => SuggestionId): Plugin<any>;
7
+ export declare function suggestStructureChanges(docBefore: Node, docAfter: Node, generateId?: (schema: Schema, doc?: Node) => SuggestionId): Transform;