@magic-marker/prosemirror-suggest-changes 0.2.1-wrap-unwrap.2 → 0.3.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__/playwrightBaseTest.d.ts +6 -0
- package/dist/__tests__/playwrightPage.d.ts +15 -0
- package/dist/commands.js +1 -10
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/decorations.js +4 -3
- package/dist/ensureSelectionPlugin.d.ts +13 -0
- package/dist/ensureSelectionPlugin.js +306 -0
- package/dist/generateId.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin.js +2 -14
- package/dist/prependDeletionsWithZWSP.d.ts +2 -0
- package/dist/prependDeletionsWithZWSP.js +45 -0
- package/dist/replaceAroundStep.js +1 -6
- package/dist/replaceStep.js +3 -7
- package/dist/schema.d.ts +4 -2
- package/dist/schema.js +32 -39
- package/dist/testing/testBuilders.d.ts +2 -1
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +2 -6
- package/dist/withSuggestChanges.js +4 -0
- package/package.json +1 -1
- package/dist/contentBetween.d.ts +0 -2
- package/dist/contentBetween.js +0 -33
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.data.d.ts +0 -38
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.data.d.ts +0 -45
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.data.d.ts +0 -38
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.data.d.ts +0 -38
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.data.d.ts +0 -38
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.data.d.ts +0 -54
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.data.d.ts +0 -48
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.data.d.ts +0 -74
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.data.d.ts +0 -71
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.data.d.ts +0 -54
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.data.d.ts +0 -221
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.page.d.ts +0 -16
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.data.d.ts +0 -51
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.data.d.ts +0 -74
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.data.d.ts +0 -71
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.data.d.ts +0 -114
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/testUtils.d.ts +0 -5
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.data.d.ts +0 -44
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.data.d.ts +0 -38
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.data.d.ts +0 -46
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.data.d.ts +0 -57
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.data.d.ts +0 -45
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.data.d.ts +0 -44
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.data.d.ts +0 -44
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.playwright.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.test.d.ts +0 -1
- package/dist/features/wrapUnwrap/handleStructureStep.d.ts +0 -4
- package/dist/features/wrapUnwrap/handleStructureStep.js +0 -630
- package/dist/features/wrapUnwrap/revertStructureSuggestion.d.ts +0 -44
- package/dist/features/wrapUnwrap/revertStructureSuggestion.js +0 -368
- package/dist/rebaseStep.d.ts +0 -9
- package/dist/rebaseStep.js +0 -11
- package/src/features/wrapUnwrap/README.md +0 -198
- /package/dist/features/{wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.playwright.test.d.ts → hiddenDeletions/__tests__/hiddenDeletionsBehavior.playwright.test.d.ts} +0 -0
- /package/dist/features/{wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.test.d.ts → joinBlocks/__tests__/joinBlockBackspace.playwright.test.d.ts} +0 -0
- /package/dist/features/{wrapUnwrap/__tests__/blockquoteUnwrapOneNode.playwright.test.d.ts → joinBlocks/__tests__/joinBlockDelete.playwright.test.d.ts} +0 -0
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { suggestChangesKey } from "../../plugin.js";
|
|
2
|
-
import { getSuggestionMarks } from "../../utils.js";
|
|
3
|
-
import { Slice } from "prosemirror-model";
|
|
4
|
-
import { ReplaceAroundStep, ReplaceStep } from "prosemirror-transform";
|
|
5
|
-
export function isStructureSuggestion(suggestionId, tr) {
|
|
6
|
-
try {
|
|
7
|
-
findStructureMarkGroupBySuggestionId(suggestionId, tr);
|
|
8
|
-
return true;
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
-
} catch (error) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function revertAllStructureSuggestions(doc, tr) {
|
|
15
|
-
const { structure } = getSuggestionMarks(doc.type.schema);
|
|
16
|
-
// find structure mark in the doc
|
|
17
|
-
// if found, revert it, produce a new doc
|
|
18
|
-
// find next structure mark in the new doc
|
|
19
|
-
// easier than going nodesBetween on the original doc,
|
|
20
|
-
// and then mapping the position, figuring out if the node is present in the new doc or was deleted
|
|
21
|
-
const findStructureMark = (doc)=>{
|
|
22
|
-
let structureMark = null;
|
|
23
|
-
doc.nodesBetween(0, doc.content.size, (node)=>{
|
|
24
|
-
structureMark = structureMark ?? structure.isInSet(node.marks) ?? null;
|
|
25
|
-
return structureMark === null;
|
|
26
|
-
});
|
|
27
|
-
return structureMark;
|
|
28
|
-
};
|
|
29
|
-
let curDoc = doc;
|
|
30
|
-
let structureMark = findStructureMark(doc);
|
|
31
|
-
while(structureMark !== null){
|
|
32
|
-
const suggestionId = structureMark.attrs["id"];
|
|
33
|
-
performStructureRevert(suggestionId, tr);
|
|
34
|
-
curDoc = tr.doc;
|
|
35
|
-
structureMark = findStructureMark(curDoc);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export function revertStructureSuggestion(suggestionId) {
|
|
39
|
-
return (state, dispatch)=>{
|
|
40
|
-
const tr = state.tr;
|
|
41
|
-
performStructureRevert(suggestionId, tr);
|
|
42
|
-
if (!tr.steps.length) return false;
|
|
43
|
-
tr.setMeta(suggestChangesKey, {
|
|
44
|
-
skip: true
|
|
45
|
-
});
|
|
46
|
-
dispatch?.(tr);
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
export function applyStructureSuggestion(suggestionId) {
|
|
51
|
-
return (state, dispatch)=>{
|
|
52
|
-
const tr = state.tr;
|
|
53
|
-
performStructureRevert(suggestionId, tr, "apply");
|
|
54
|
-
if (!tr.steps.length) return false;
|
|
55
|
-
tr.setMeta(suggestChangesKey, {
|
|
56
|
-
skip: true
|
|
57
|
-
});
|
|
58
|
-
dispatch?.(tr);
|
|
59
|
-
return true;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
export function revertStructureSuggestions(suggestionIds) {
|
|
63
|
-
return (state, dispatch)=>{
|
|
64
|
-
const tr = state.tr;
|
|
65
|
-
suggestionIds.forEach((suggestionId)=>{
|
|
66
|
-
performStructureRevert(suggestionId, tr);
|
|
67
|
-
});
|
|
68
|
-
if (!tr.steps.length) return false;
|
|
69
|
-
tr.setMeta(suggestChangesKey, {
|
|
70
|
-
skip: true
|
|
71
|
-
});
|
|
72
|
-
dispatch?.(tr);
|
|
73
|
-
return true;
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function performStructureRevert(suggestionId, tr, direction = "revert") {
|
|
77
|
-
console.groupCollapsed(// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
78
|
-
`performStructureRevert, suggestionId = ${suggestionId}`);
|
|
79
|
-
const { structure } = getSuggestionMarks(tr.doc.type.schema);
|
|
80
|
-
// find main suggestion from and to
|
|
81
|
-
const { markFrom, markTo, markGapFrom, markGapTo } = findStructureMarkGroupBySuggestionId(suggestionId, tr);
|
|
82
|
-
const from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
|
|
83
|
-
const to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
|
|
84
|
-
if (from == null || to == null) {
|
|
85
|
-
throw new Error(`Could not find all positions for suggestion`);
|
|
86
|
-
}
|
|
87
|
-
// find all other structure suggestions within from and to interval of the main suggestion
|
|
88
|
-
const structureMarkGroups = new Set();
|
|
89
|
-
structureMarkGroups.add(suggestionId);
|
|
90
|
-
if (markGapFrom != null) {
|
|
91
|
-
const gapFrom = getPosFromMark(markGapFrom.mark, markGapFrom.pos, markGapFrom.node);
|
|
92
|
-
const gapTo = getPosFromMark(markGapTo.mark, markGapTo.pos, markGapTo.node);
|
|
93
|
-
if (gapFrom == null || gapTo == null) {
|
|
94
|
-
throw new Error(`Could not find all positions for suggestion`);
|
|
95
|
-
}
|
|
96
|
-
tr.doc.nodesBetween(from, to, (node, pos)=>{
|
|
97
|
-
node.marks.forEach((mark)=>{
|
|
98
|
-
if (mark.type !== structure) return;
|
|
99
|
-
const markData = mark.attrs["data"];
|
|
100
|
-
if (!markData) return;
|
|
101
|
-
const id = mark.attrs["id"];
|
|
102
|
-
if (id === suggestionId) return;
|
|
103
|
-
const startsInGap = gapFrom <= pos && pos <= gapTo;
|
|
104
|
-
const endsInGap = gapFrom <= pos + node.nodeSize && pos + node.nodeSize <= gapTo;
|
|
105
|
-
if (startsInGap && endsInGap) {
|
|
106
|
-
console.log("ignored mark", {
|
|
107
|
-
node,
|
|
108
|
-
pos,
|
|
109
|
-
end: pos + node.nodeSize,
|
|
110
|
-
mark
|
|
111
|
-
}, " - starts and ends in gap", "from-gapFrom-gapTo-to", {
|
|
112
|
-
from,
|
|
113
|
-
gapFrom,
|
|
114
|
-
gapTo,
|
|
115
|
-
to
|
|
116
|
-
});
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const startsInRange = from <= pos && pos <= gapFrom || gapTo <= pos && pos <= to;
|
|
120
|
-
const endsInRange = from <= pos + node.nodeSize && pos + node.nodeSize <= gapFrom || gapTo <= pos + node.nodeSize && pos + node.nodeSize <= to;
|
|
121
|
-
if (!startsInRange && !endsInRange) {
|
|
122
|
-
console.log("ignored mark", {
|
|
123
|
-
node,
|
|
124
|
-
pos,
|
|
125
|
-
end: pos + node.nodeSize,
|
|
126
|
-
mark
|
|
127
|
-
}, " - does not start or end in range", "from-gapFrom-gapTo-to", {
|
|
128
|
-
from,
|
|
129
|
-
gapFrom,
|
|
130
|
-
gapTo,
|
|
131
|
-
to
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
console.log("found nested structure mark", {
|
|
136
|
-
id,
|
|
137
|
-
node,
|
|
138
|
-
pos,
|
|
139
|
-
nodeStart: pos,
|
|
140
|
-
nodeEnd: pos + node.nodeSize
|
|
141
|
-
}, "from-gapFrom-gapTo-to", {
|
|
142
|
-
from,
|
|
143
|
-
gapFrom,
|
|
144
|
-
gapTo,
|
|
145
|
-
to
|
|
146
|
-
});
|
|
147
|
-
structureMarkGroups.add(id);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
} else {
|
|
151
|
-
tr.doc.nodesBetween(from, to, (node, pos)=>{
|
|
152
|
-
node.marks.forEach((mark)=>{
|
|
153
|
-
if (mark.type !== structure) return;
|
|
154
|
-
const markData = mark.attrs["data"];
|
|
155
|
-
if (!markData) return;
|
|
156
|
-
const id = mark.attrs["id"];
|
|
157
|
-
if (id === suggestionId) return;
|
|
158
|
-
const startsInRange = from <= pos && pos <= to;
|
|
159
|
-
const endsInRange = from <= pos + node.nodeSize && pos + node.nodeSize <= to;
|
|
160
|
-
if (!startsInRange && !endsInRange) {
|
|
161
|
-
console.log("ignored mark", {
|
|
162
|
-
node,
|
|
163
|
-
pos,
|
|
164
|
-
end: pos + node.nodeSize,
|
|
165
|
-
mark
|
|
166
|
-
}, " - does not start or end in range", "from-to", {
|
|
167
|
-
from,
|
|
168
|
-
to
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
console.log("found nested structure mark", {
|
|
173
|
-
id,
|
|
174
|
-
node,
|
|
175
|
-
pos,
|
|
176
|
-
nodeStart: pos,
|
|
177
|
-
nodeEnd: pos + node.nodeSize
|
|
178
|
-
}, "from-to", {
|
|
179
|
-
from,
|
|
180
|
-
to
|
|
181
|
-
});
|
|
182
|
-
structureMarkGroups.add(id);
|
|
183
|
-
});
|
|
184
|
-
return true;
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// revert structure mark groups in decreasing order of their ids
|
|
188
|
-
const markIds = Array.from(structureMarkGroups.values()).sort((a, b)=>Number(b) - Number(a));
|
|
189
|
-
markIds.forEach((id)=>{
|
|
190
|
-
const group = findStructureMarkGroupBySuggestionId(id, tr);
|
|
191
|
-
if (direction === "apply") {
|
|
192
|
-
applyStructureMarkGroup(group, tr);
|
|
193
|
-
} else {
|
|
194
|
-
revertStructureMarkGroup(group, tr);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
console.groupEnd();
|
|
198
|
-
}
|
|
199
|
-
function getPosFromMark(mark, pos, node) {
|
|
200
|
-
const markData = mark.attrs["data"];
|
|
201
|
-
if (!markData) return null;
|
|
202
|
-
if (markData.position === "start") {
|
|
203
|
-
return pos;
|
|
204
|
-
}
|
|
205
|
-
if (markData.position === "end") {
|
|
206
|
-
return pos + node.nodeSize;
|
|
207
|
-
}
|
|
208
|
-
if (markData.position === "innerStart") {
|
|
209
|
-
return pos + 1;
|
|
210
|
-
}
|
|
211
|
-
if (markData.position === "innerEnd") {
|
|
212
|
-
return pos + node.nodeSize - 1;
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
export function applyStructureMarkGroup(group, tr) {
|
|
217
|
-
console.groupCollapsed("apply structure group, id = ", group.markFrom.mark.attrs["id"]);
|
|
218
|
-
console.log({
|
|
219
|
-
group
|
|
220
|
-
});
|
|
221
|
-
if (group.type === "replace") {
|
|
222
|
-
const { markFrom, markTo } = group;
|
|
223
|
-
tr.removeNodeMark(markFrom.pos, markFrom.mark);
|
|
224
|
-
tr.removeNodeMark(markTo.pos, markTo.mark);
|
|
225
|
-
console.groupEnd();
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const { markFrom, markTo, markGapFrom, markGapTo } = group;
|
|
229
|
-
tr.removeNodeMark(markFrom.pos, markFrom.mark);
|
|
230
|
-
tr.removeNodeMark(markTo.pos, markTo.mark);
|
|
231
|
-
tr.removeNodeMark(markGapFrom.pos, markGapFrom.mark);
|
|
232
|
-
tr.removeNodeMark(markGapTo.pos, markGapTo.mark);
|
|
233
|
-
console.groupEnd();
|
|
234
|
-
}
|
|
235
|
-
function revertStructureMarkGroup(group, tr) {
|
|
236
|
-
console.groupCollapsed("revert structure group, id = ", group.markFrom.mark.attrs["id"]);
|
|
237
|
-
console.log({
|
|
238
|
-
group
|
|
239
|
-
});
|
|
240
|
-
if (group.type === "replace") {
|
|
241
|
-
const { markFrom, markTo } = group;
|
|
242
|
-
// extract positions from, to, gapFrom and gapTo from marks
|
|
243
|
-
// the positions are either the mark's start, or the mark's end
|
|
244
|
-
const from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
|
|
245
|
-
const to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
|
|
246
|
-
if (from === null || to === null) {
|
|
247
|
-
throw new Error(`Could not find all positions for suggestion`);
|
|
248
|
-
}
|
|
249
|
-
// extract the rest of the data required to reconstruct the step: slice, and structure
|
|
250
|
-
// any of those 2 marks can be used for that, this data is identical in all of them
|
|
251
|
-
const mark = markFrom.mark;
|
|
252
|
-
const markData = mark.attrs["data"];
|
|
253
|
-
if (!markData) {
|
|
254
|
-
throw new Error(`Missing mark data for suggestion`);
|
|
255
|
-
}
|
|
256
|
-
const slice = markData.slice ? Slice.fromJSON(tr.doc.type.schema, markData.slice) : Slice.empty;
|
|
257
|
-
const isStepStructural = markData.structure ?? false;
|
|
258
|
-
// reconstruct the step
|
|
259
|
-
// this is the inverse step of the step that created this change
|
|
260
|
-
const step = new ReplaceStep(from, to, slice, isStepStructural);
|
|
261
|
-
console.log({
|
|
262
|
-
step
|
|
263
|
-
});
|
|
264
|
-
tr.removeNodeMark(markFrom.pos, markFrom.mark);
|
|
265
|
-
tr.removeNodeMark(markTo.pos, markTo.mark);
|
|
266
|
-
tr.step(step);
|
|
267
|
-
console.groupEnd();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const { markFrom, markTo, markGapFrom, markGapTo } = group;
|
|
271
|
-
// extract positions from, to, gapFrom and gapTo from marks
|
|
272
|
-
// the positions are either the mark's start, or the mark's end
|
|
273
|
-
const from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
|
|
274
|
-
const to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
|
|
275
|
-
const gapFrom = getPosFromMark(markGapFrom.mark, markGapFrom.pos, markGapFrom.node);
|
|
276
|
-
const gapTo = getPosFromMark(markGapTo.mark, markGapTo.pos, markGapTo.node);
|
|
277
|
-
if (from === null || to === null || gapFrom === null || gapTo === null) {
|
|
278
|
-
throw new Error(`Could not find all positions for suggestion`);
|
|
279
|
-
}
|
|
280
|
-
// extract the rest of the data required to reconstruct the step: insert, slice, and structure
|
|
281
|
-
// any of those 4 marks can be used for that, this data is identical in all of them
|
|
282
|
-
const mark = markGapFrom.mark;
|
|
283
|
-
const markData = mark.attrs["data"];
|
|
284
|
-
if (markData?.insert == null) {
|
|
285
|
-
throw new Error(`Missing insert for suggestion`);
|
|
286
|
-
}
|
|
287
|
-
const slice = markData.slice ? Slice.fromJSON(tr.doc.type.schema, markData.slice) : Slice.empty;
|
|
288
|
-
const insert = markData.insert;
|
|
289
|
-
const isStepStructural = markData.structure ?? false;
|
|
290
|
-
// reconstruct the step
|
|
291
|
-
// this is the inverse step of the step that created this change
|
|
292
|
-
const step = new ReplaceAroundStep(from, to, gapFrom, gapTo, slice, insert, isStepStructural);
|
|
293
|
-
console.log({
|
|
294
|
-
step
|
|
295
|
-
});
|
|
296
|
-
tr.removeNodeMark(markFrom.pos, markFrom.mark);
|
|
297
|
-
tr.removeNodeMark(markTo.pos, markTo.mark);
|
|
298
|
-
tr.removeNodeMark(markGapFrom.pos, markGapFrom.mark);
|
|
299
|
-
tr.removeNodeMark(markGapTo.pos, markGapTo.mark);
|
|
300
|
-
tr.step(step);
|
|
301
|
-
console.groupEnd();
|
|
302
|
-
}
|
|
303
|
-
function findStructureMarkGroupBySuggestionId(suggestionId, tr) {
|
|
304
|
-
const { structure } = getSuggestionMarks(tr.doc.type.schema);
|
|
305
|
-
let markFrom = null;
|
|
306
|
-
let markTo = null;
|
|
307
|
-
let markGapFrom = null;
|
|
308
|
-
let markGapTo = null;
|
|
309
|
-
// using suggestionId, find 4 marks: from, to, gapFrom and gapTo
|
|
310
|
-
tr.doc.nodesBetween(0, tr.doc.content.size, (node, pos)=>{
|
|
311
|
-
node.marks.forEach((mark)=>{
|
|
312
|
-
if (mark.type !== structure) return;
|
|
313
|
-
if (mark.attrs["id"] !== suggestionId) return;
|
|
314
|
-
const markData = mark.attrs["data"];
|
|
315
|
-
if (!markData) return;
|
|
316
|
-
if (markData.value === "from") {
|
|
317
|
-
markFrom = {
|
|
318
|
-
pos,
|
|
319
|
-
node,
|
|
320
|
-
mark
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
if (markData.value === "to") {
|
|
324
|
-
markTo = {
|
|
325
|
-
pos,
|
|
326
|
-
node,
|
|
327
|
-
mark
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
if (markData.value === "gapFrom") {
|
|
331
|
-
markGapFrom = {
|
|
332
|
-
pos,
|
|
333
|
-
node,
|
|
334
|
-
mark
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
if (markData.value === "gapTo") {
|
|
338
|
-
markGapTo = {
|
|
339
|
-
pos,
|
|
340
|
-
node,
|
|
341
|
-
mark
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
return markFrom == null || markTo == null || markGapFrom == null || markGapTo == null;
|
|
346
|
-
});
|
|
347
|
-
if (markFrom == null || markTo == null) {
|
|
348
|
-
throw new Error(`Could not find all marks for suggestion id ${suggestionId}`);
|
|
349
|
-
}
|
|
350
|
-
const type = markFrom.mark.attrs["data"]?.type;
|
|
351
|
-
if (type === "replace") {
|
|
352
|
-
return {
|
|
353
|
-
type: "replace",
|
|
354
|
-
markFrom,
|
|
355
|
-
markTo
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
if (markGapFrom == null || markGapTo == null) {
|
|
359
|
-
throw new Error(`Could not find all gap marks for replace around suggestion id ${suggestionId}`);
|
|
360
|
-
}
|
|
361
|
-
return {
|
|
362
|
-
type: "replaceAround",
|
|
363
|
-
markFrom,
|
|
364
|
-
markTo,
|
|
365
|
-
markGapFrom,
|
|
366
|
-
markGapTo
|
|
367
|
-
};
|
|
368
|
-
}
|
package/dist/rebaseStep.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type Step } from "prosemirror-transform";
|
|
2
|
-
/**
|
|
3
|
-
* Rebase a step onto a new lineage of steps
|
|
4
|
-
*
|
|
5
|
-
* @param step The step to rebase
|
|
6
|
-
* @param back The old steps to undo, in the order they were originally applied
|
|
7
|
-
* @param forth The new steps to map through
|
|
8
|
-
*/
|
|
9
|
-
export declare function rebaseStep(step: Step, back: Step[], forth: Step[]): Step | null;
|
package/dist/rebaseStep.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rebase a step onto a new lineage of steps
|
|
3
|
-
*
|
|
4
|
-
* @param step The step to rebase
|
|
5
|
-
* @param back The old steps to undo, in the order they were originally applied
|
|
6
|
-
* @param forth The new steps to map through
|
|
7
|
-
*/ export function rebaseStep(step, back, forth) {
|
|
8
|
-
const reset = back.slice().reverse().reduce((acc, step)=>acc?.map(step.getMap().invert()) ?? null, step);
|
|
9
|
-
const rebased = forth.reduce((acc, step)=>acc?.map(step.getMap()) ?? null, reset);
|
|
10
|
-
return rebased;
|
|
11
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
# Structure Suggestion Tracking & Reversal
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
Track structural changes (wrap/unwrap in blockquotes, list item lift/sink) in
|
|
6
|
-
ProseMirror as "suggestions" that can be reverted later, without losing the
|
|
7
|
-
ability to reconstruct the original document state.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Why Marks on Nodes?
|
|
12
|
-
|
|
13
|
-
**The Problem**: Document positions are ephemeral. A position like `from: 10`
|
|
14
|
-
becomes invalid after any edit before it.
|
|
15
|
-
|
|
16
|
-
**The Solution**: Anchor positions to **actual nodes** in the document using
|
|
17
|
-
marks. To recover a position:
|
|
18
|
-
|
|
19
|
-
1. Find the marked node (which moves with document edits)
|
|
20
|
-
2. Compute position from node's current `pos` + the `position` attribute:
|
|
21
|
-
- `position: "start"` → use `pos`
|
|
22
|
-
- `position: "end"` → use `pos + node.nodeSize`
|
|
23
|
-
|
|
24
|
-
This way, positions are **self-healing** — they remain valid as long as the
|
|
25
|
-
marked nodes exist.
|
|
26
|
-
|
|
27
|
-
**Why "start" vs "end"?** Because the 4 positions of a `ReplaceAroundStep` don't
|
|
28
|
-
always align with node boundaries in the same way. The mark must be placed on a
|
|
29
|
-
node that **exists in the final document**, and the position must be derivable
|
|
30
|
-
from that node's boundaries.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Mark Structure
|
|
35
|
-
|
|
36
|
-
### For `ReplaceAroundStep` (4 marks)
|
|
37
|
-
|
|
38
|
-
Each mark contains:
|
|
39
|
-
|
|
40
|
-
| Field | Purpose |
|
|
41
|
-
| ----------- | ------------------------------------------------------- |
|
|
42
|
-
| `id` | Suggestion ID (groups marks together) |
|
|
43
|
-
| `value` | `"from"` \| `"to"` \| `"gapFrom"` \| `"gapTo"` |
|
|
44
|
-
| `position` | `"start"` \| `"end"` (how to derive position from node) |
|
|
45
|
-
| `type` | `"replaceAround"` |
|
|
46
|
-
| `slice` | JSON of the slice for the **inverse** step |
|
|
47
|
-
| `insert` | Where gap content goes in the slice |
|
|
48
|
-
| `structure` | Boolean flag for structural step |
|
|
49
|
-
|
|
50
|
-
### For `ReplaceStep` (2 marks)
|
|
51
|
-
|
|
52
|
-
Only `from` and `to` marks are needed (no gap positions).
|
|
53
|
-
|
|
54
|
-
| Field | Purpose |
|
|
55
|
-
| ----------- | ------------------------------------------ |
|
|
56
|
-
| `id` | Suggestion ID |
|
|
57
|
-
| `value` | `"from"` \| `"to"` |
|
|
58
|
-
| `position` | `"start"` \| `"end"` |
|
|
59
|
-
| `type` | `"replace"` |
|
|
60
|
-
| `slice` | JSON of the slice for the **inverse** step |
|
|
61
|
-
| `structure` | Boolean flag |
|
|
62
|
-
|
|
63
|
-
### Mark Nesting
|
|
64
|
-
|
|
65
|
-
Marks are nested around content in the document:
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
doc(
|
|
69
|
-
structure[from, position:start](
|
|
70
|
-
structure[to, position:end](
|
|
71
|
-
blockquote(
|
|
72
|
-
structure[gapFrom, position:start](
|
|
73
|
-
structure[gapTo, position:end](
|
|
74
|
-
paragraph("Hello World")
|
|
75
|
-
))))))
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
When traversing with `nodesBetween`, we find each marked node and compute
|
|
79
|
-
positions from it.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## How Reversal Works
|
|
84
|
-
|
|
85
|
-
The `revertStructureSuggestion(suggestionId)` command:
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
1. FIND the main suggestion's marks
|
|
89
|
-
└── findStructureMarkGroupBySuggestionId(suggestionId)
|
|
90
|
-
└── Scan document for marks with matching ID
|
|
91
|
-
└── Collect: markFrom, markTo, [markGapFrom, markGapTo]
|
|
92
|
-
|
|
93
|
-
2. FIND nested suggestions within the range
|
|
94
|
-
└── nodesBetween(from, to) to find all structure marks
|
|
95
|
-
└── Collect all unique suggestion IDs
|
|
96
|
-
|
|
97
|
-
3. SORT by decreasing ID (revert newest first)
|
|
98
|
-
└── [3, 2, 1] — important for nested changes!
|
|
99
|
-
|
|
100
|
-
4. For EACH suggestion, REVERT:
|
|
101
|
-
└── Compute positions: getPosFromMark(mark, pos, node)
|
|
102
|
-
└── Deserialize slice from JSON
|
|
103
|
-
└── Reconstruct inverse step:
|
|
104
|
-
- ReplaceAroundStep(from, to, gapFrom, gapTo, slice, insert, structure)
|
|
105
|
-
- or ReplaceStep(from, to, slice, structure)
|
|
106
|
-
└── Remove the marks
|
|
107
|
-
└── Apply the step
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Why reverse order?** If step 1 created structure A, and step 2 modified within
|
|
111
|
-
A, you must undo step 2 before step 1, otherwise positions break.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## How Tests Work
|
|
116
|
-
|
|
117
|
-
Each test file has 3 data structures:
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// 1. Initial document state (before any changes)
|
|
121
|
-
const initialState = doc(paragraph("Hello World"));
|
|
122
|
-
|
|
123
|
-
// 2. Final state (after applying steps, NO marks)
|
|
124
|
-
const finalState = doc(blockquote(paragraph("Hello World")));
|
|
125
|
-
|
|
126
|
-
// 3. Final state WITH structure marks embedded
|
|
127
|
-
// (manually constructed to simulate what forward tracking would produce)
|
|
128
|
-
const finalStateWithMarks = doc(
|
|
129
|
-
structure({ id: 1, data: { value: "from", position: "start", ... } },
|
|
130
|
-
structure({ id: 1, data: { value: "to", position: "end", ... } },
|
|
131
|
-
blockquote(
|
|
132
|
-
structure({ id: 1, data: { value: "gapFrom", ... } },
|
|
133
|
-
structure({ id: 1, data: { value: "gapTo", ... } },
|
|
134
|
-
paragraph("Hello World")
|
|
135
|
-
)))))
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// The steps that transform initial → final
|
|
139
|
-
const steps = [{ stepType: "replaceAround", from: 0, to: 13, ... }];
|
|
140
|
-
|
|
141
|
-
// The inverse steps that transform final → initial
|
|
142
|
-
const inverseSteps = [{ stepType: "replaceAround", from: 0, to: 15, ... }];
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Three test cases per scenario:
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
// Test 1: Forward transformation works
|
|
149
|
-
it("applies steps correctly", () => {
|
|
150
|
-
assertDocumentChanged(initialState, finalState, applySteps(steps));
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Test 2: Inverse steps work (sanity check)
|
|
154
|
-
it("applies inverse steps correctly", () => {
|
|
155
|
-
assertDocumentChanged(
|
|
156
|
-
finalState,
|
|
157
|
-
initialState,
|
|
158
|
-
applySteps(inverseSteps.reverse()),
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Test 3: THE MAIN TEST - revert from marks works
|
|
163
|
-
it("reverts via structure suggestion", () => {
|
|
164
|
-
assertDocumentChanged(
|
|
165
|
-
finalStateWithMarks,
|
|
166
|
-
initialState,
|
|
167
|
-
revertStructureSuggestion(1),
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## What's Missing: Forward Tracking
|
|
175
|
-
|
|
176
|
-
The code that would:
|
|
177
|
-
|
|
178
|
-
1. Intercept `ReplaceAroundStep` / `ReplaceStep` in suggest-changes mode
|
|
179
|
-
2. Apply the step to get the new document
|
|
180
|
-
3. Compute the inverse step
|
|
181
|
-
4. Place structure marks on appropriate nodes with:
|
|
182
|
-
- Position encoding (which node, start vs end)
|
|
183
|
-
- Inverse step data (slice, insert, structure flag)
|
|
184
|
-
|
|
185
|
-
Currently `suggestReplaceAroundStep` in `replaceAroundStep.ts` converts
|
|
186
|
-
structural operations to `ReplaceStep` and uses insertion/deletion marks
|
|
187
|
-
instead, losing the structural information needed for proper reversal.
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## Additional Notes
|
|
192
|
-
|
|
193
|
-
### Offset Fields
|
|
194
|
-
|
|
195
|
-
The marks contain offset fields (`gapFromOffset`, `fromOffset`, `toOffset`,
|
|
196
|
-
`gapToOffset`) and a `debug` object with computed inverse positions. These are
|
|
197
|
-
present in test data but **not currently used** by the revert code — positions
|
|
198
|
-
are computed purely from node position + `position` field.
|
|
File without changes
|
|
File without changes
|