@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.
- package/dist/__tests__/playwrightPage.d.ts +8 -1
- package/dist/commands.js +132 -41
- package/dist/ensureSelectionPlugin.js +3 -2
- package/dist/features/wrapUnwrap/__tests__/listStructure.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/addIdAttr.d.ts +2 -0
- package/dist/features/wrapUnwrap/addIdAttr.js +62 -0
- package/dist/features/wrapUnwrap/buildMaterializedPaths.d.ts +3 -0
- package/dist/features/wrapUnwrap/buildMaterializedPaths.js +71 -0
- package/dist/features/wrapUnwrap/constants.d.ts +3 -0
- package/dist/features/wrapUnwrap/constants.js +7 -0
- package/dist/features/wrapUnwrap/generateNodeId.d.ts +2 -0
- package/dist/features/wrapUnwrap/generateNodeId.js +6 -0
- package/dist/features/wrapUnwrap/getNodeId.d.ts +2 -0
- package/dist/features/wrapUnwrap/getNodeId.js +5 -0
- package/dist/features/wrapUnwrap/revertStructureSuggestions.d.ts +9 -0
- package/dist/features/wrapUnwrap/revertStructureSuggestions.js +433 -0
- package/dist/features/wrapUnwrap/sameParentChain.d.ts +2 -0
- package/dist/features/wrapUnwrap/sameParentChain.js +4 -0
- package/dist/features/wrapUnwrap/stableNodeIdsPlugin.d.ts +12 -0
- package/dist/features/wrapUnwrap/stableNodeIdsPlugin.js +95 -0
- package/dist/features/wrapUnwrap/structureChangesPlugin.d.ts +7 -0
- package/dist/features/wrapUnwrap/structureChangesPlugin.js +174 -0
- package/dist/features/wrapUnwrap/types.d.ts +45 -0
- package/dist/features/wrapUnwrap/types.js +13 -0
- package/dist/generateId.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/rebaseStep.d.ts +9 -0
- package/dist/rebaseStep.js +11 -0
- package/dist/replaceStep.d.ts +1 -1
- package/dist/schema.d.ts +2 -1
- package/dist/schema.js +37 -1
- package/dist/testing/e2eTestSchema.d.ts +2 -0
- package/dist/testing/testBuilders.d.ts +1 -2
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +6 -2
- package/dist/withSuggestChanges.d.ts +5 -1
- package/dist/withSuggestChanges.js +39 -2
- package/package.json +2 -2
- 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,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;
|