@magic-marker/prosemirror-suggest-changes 0.3.1 → 0.3.3-wrap-unwrap.0

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 (94) hide show
  1. package/dist/commands.d.ts +3 -1
  2. package/dist/commands.js +14 -2
  3. package/dist/contentBetween.d.ts +2 -0
  4. package/dist/contentBetween.js +33 -0
  5. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.data.d.ts +38 -0
  6. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.playwright.test.d.ts +1 -0
  7. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.test.d.ts +1 -0
  8. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.data.d.ts +45 -0
  9. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.playwright.test.d.ts +1 -0
  10. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.test.d.ts +1 -0
  11. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.data.d.ts +38 -0
  12. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.playwright.test.d.ts +1 -0
  13. package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.test.d.ts +1 -0
  14. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.data.d.ts +38 -0
  15. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.playwright.test.d.ts +1 -0
  16. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.test.d.ts +1 -0
  17. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.data.d.ts +38 -0
  18. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.playwright.test.d.ts +1 -0
  19. package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.test.d.ts +1 -0
  20. package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.data.d.ts +54 -0
  21. package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.playwright.test.d.ts +1 -0
  22. package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.test.d.ts +1 -0
  23. package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.data.d.ts +48 -0
  24. package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.playwright.test.d.ts +1 -0
  25. package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.test.d.ts +1 -0
  26. package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.data.d.ts +74 -0
  27. package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.playwright.test.d.ts +1 -0
  28. package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.test.d.ts +1 -0
  29. package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.data.d.ts +71 -0
  30. package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.playwright.test.d.ts +1 -0
  31. package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.test.d.ts +1 -0
  32. package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.data.d.ts +54 -0
  33. package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.playwright.test.d.ts +1 -0
  34. package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.test.d.ts +1 -0
  35. package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.data.d.ts +181 -0
  36. package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.page.d.ts +30 -0
  37. package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.playwright.test.d.ts +1 -0
  38. package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.test.d.ts +1 -0
  39. package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.data.d.ts +51 -0
  40. package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.playwright.test.d.ts +1 -0
  41. package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.test.d.ts +1 -0
  42. package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.data.d.ts +74 -0
  43. package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.playwright.test.d.ts +1 -0
  44. package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.test.d.ts +1 -0
  45. package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.data.d.ts +71 -0
  46. package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.playwright.test.d.ts +1 -0
  47. package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.test.d.ts +1 -0
  48. package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.data.d.ts +114 -0
  49. package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.playwright.test.d.ts +1 -0
  50. package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.test.d.ts +1 -0
  51. package/dist/features/wrapUnwrap/__tests__/testUtils.d.ts +5 -0
  52. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.data.d.ts +44 -0
  53. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.playwright.test.d.ts +1 -0
  54. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.test.d.ts +1 -0
  55. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.data.d.ts +38 -0
  56. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.playwright.test.d.ts +1 -0
  57. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.test.d.ts +1 -0
  58. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.data.d.ts +46 -0
  59. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.playwright.test.d.ts +1 -0
  60. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.test.d.ts +1 -0
  61. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.data.d.ts +57 -0
  62. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.playwright.test.d.ts +1 -0
  63. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.test.d.ts +1 -0
  64. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.data.d.ts +45 -0
  65. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.playwright.test.d.ts +1 -0
  66. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.test.d.ts +1 -0
  67. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.data.d.ts +44 -0
  68. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.playwright.test.d.ts +1 -0
  69. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.test.d.ts +1 -0
  70. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.data.d.ts +44 -0
  71. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.playwright.test.d.ts +1 -0
  72. package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.test.d.ts +1 -0
  73. package/dist/features/wrapUnwrap/addStructureMark.d.ts +41 -0
  74. package/dist/features/wrapUnwrap/addStructureMark.js +15 -0
  75. package/dist/features/wrapUnwrap/findMatchingNodeSides.d.ts +15 -0
  76. package/dist/features/wrapUnwrap/findMatchingNodeSides.js +133 -0
  77. package/dist/features/wrapUnwrap/handleStructureStep.d.ts +4 -0
  78. package/dist/features/wrapUnwrap/handleStructureStep.js +174 -0
  79. package/dist/features/wrapUnwrap/revertStructureSuggestion.d.ts +44 -0
  80. package/dist/features/wrapUnwrap/revertStructureSuggestion.js +374 -0
  81. package/dist/generateId.js +2 -2
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/rebaseStep.d.ts +9 -0
  85. package/dist/rebaseStep.js +11 -0
  86. package/dist/replaceAroundStep.js +6 -1
  87. package/dist/replaceStep.js +5 -0
  88. package/dist/schema.d.ts +2 -1
  89. package/dist/schema.js +37 -1
  90. package/dist/testing/testBuilders.d.ts +1 -2
  91. package/dist/utils.d.ts +1 -0
  92. package/dist/utils.js +6 -2
  93. package/package.json +1 -1
  94. package/src/features/wrapUnwrap/README.md +198 -0
@@ -0,0 +1,44 @@
1
+ import { type Transaction, type Command } from "prosemirror-state";
2
+ import { type SuggestionId } from "../../generateId.js";
3
+ import { type Node, type Mark } from "prosemirror-model";
4
+ import { type Transform } from "prosemirror-transform";
5
+ export declare function isStructureSuggestion(suggestionId: SuggestionId, tr: Transaction): boolean;
6
+ export declare function revertAllStructureSuggestions(doc: Node, tr: Transaction): void;
7
+ export declare function revertStructureSuggestion(suggestionId: SuggestionId): Command;
8
+ export declare function applyStructureSuggestion(suggestionId: SuggestionId): Command;
9
+ export declare function revertStructureSuggestions(suggestionIds: SuggestionId[]): Command;
10
+ export declare function applyStructureMarkGroup(group: {
11
+ type: "replaceAround";
12
+ markFrom: {
13
+ pos: number;
14
+ node: Node;
15
+ mark: Mark;
16
+ };
17
+ markTo: {
18
+ pos: number;
19
+ node: Node;
20
+ mark: Mark;
21
+ };
22
+ markGapFrom: {
23
+ pos: number;
24
+ node: Node;
25
+ mark: Mark;
26
+ };
27
+ markGapTo: {
28
+ pos: number;
29
+ node: Node;
30
+ mark: Mark;
31
+ };
32
+ } | {
33
+ type: "replace";
34
+ markFrom: {
35
+ pos: number;
36
+ node: Node;
37
+ mark: Mark;
38
+ };
39
+ markTo: {
40
+ pos: number;
41
+ node: Node;
42
+ mark: Mark;
43
+ };
44
+ }, tr: Transform): void;
@@ -0,0 +1,374 @@
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, and then mapping the position, figuring out if the node is present in the new doc or was deleted
20
+ const findStructureMark = (doc)=>{
21
+ let structureMark = null;
22
+ doc.nodesBetween(0, doc.content.size, (node)=>{
23
+ structureMark = structureMark ?? structure.isInSet(node.marks) ?? null;
24
+ return structureMark === null;
25
+ });
26
+ return structureMark;
27
+ };
28
+ let curDoc = doc;
29
+ let structureMark = findStructureMark(doc);
30
+ while(structureMark !== null){
31
+ const suggestionId = structureMark.attrs["id"];
32
+ performStructureRevert(suggestionId, tr);
33
+ curDoc = tr.doc;
34
+ structureMark = findStructureMark(curDoc);
35
+ }
36
+ }
37
+ export function revertStructureSuggestion(suggestionId) {
38
+ return (state, dispatch)=>{
39
+ const tr = state.tr;
40
+ performStructureRevert(suggestionId, tr);
41
+ if (!tr.steps.length) return false;
42
+ tr.setMeta(suggestChangesKey, {
43
+ skip: true
44
+ });
45
+ dispatch?.(tr);
46
+ return true;
47
+ };
48
+ }
49
+ export function applyStructureSuggestion(suggestionId) {
50
+ return (state, dispatch)=>{
51
+ const tr = state.tr;
52
+ performStructureRevert(suggestionId, tr, "apply");
53
+ if (!tr.steps.length) return false;
54
+ tr.setMeta(suggestChangesKey, {
55
+ skip: true
56
+ });
57
+ dispatch?.(tr);
58
+ return true;
59
+ };
60
+ }
61
+ export function revertStructureSuggestions(suggestionIds) {
62
+ return (state, dispatch)=>{
63
+ const tr = state.tr;
64
+ suggestionIds.forEach((suggestionId)=>{
65
+ performStructureRevert(suggestionId, tr);
66
+ });
67
+ if (!tr.steps.length) return false;
68
+ tr.setMeta(suggestChangesKey, {
69
+ skip: true
70
+ });
71
+ dispatch?.(tr);
72
+ return true;
73
+ };
74
+ }
75
+ function performStructureRevert(suggestionId, tr, direction = "revert") {
76
+ console.groupCollapsed(// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
77
+ `performStructureRevert, suggestionId = ${suggestionId}`);
78
+ const { structure } = getSuggestionMarks(tr.doc.type.schema);
79
+ // find main suggestion from and to
80
+ const { markFrom, markTo, markGapFrom, markGapTo } = findStructureMarkGroupBySuggestionId(suggestionId, tr);
81
+ console.log("found main suggestion", {
82
+ markFrom,
83
+ markTo,
84
+ markGapFrom,
85
+ markGapTo,
86
+ suggestionId
87
+ });
88
+ let from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
89
+ let to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
90
+ console.log("main suggestion positions", {
91
+ from,
92
+ to
93
+ });
94
+ if (from == null || to == null) {
95
+ throw new Error(`Could not find all positions for suggestion`);
96
+ }
97
+ [from, to] = from > to ? [
98
+ to,
99
+ from
100
+ ] : [
101
+ from,
102
+ to
103
+ ];
104
+ // find all other structure suggestions within from and to interval of the main suggestion
105
+ const structureMarkGroups = new Set();
106
+ structureMarkGroups.add(suggestionId);
107
+ let gapFrom = null;
108
+ let gapTo = null;
109
+ if (markGapFrom != null) {
110
+ gapFrom = getPosFromMark(markGapFrom.mark, markGapFrom.pos, markGapFrom.node);
111
+ gapTo = getPosFromMark(markGapTo.mark, markGapTo.pos, markGapTo.node);
112
+ if (gapFrom != null && gapTo != null) {
113
+ [gapFrom, gapTo] = gapFrom > gapTo ? [
114
+ gapTo,
115
+ gapFrom
116
+ ] : [
117
+ gapFrom,
118
+ gapTo
119
+ ];
120
+ }
121
+ }
122
+ tr.doc.nodesBetween(from, to, (node, pos)=>{
123
+ node.marks.forEach((mark)=>{
124
+ if (mark.type !== structure) return;
125
+ const markData = mark.attrs["data"];
126
+ if (!markData) return;
127
+ const id = mark.attrs["id"];
128
+ if (id <= suggestionId) return;
129
+ let startsInGap = false;
130
+ let endsInGap = false;
131
+ if (gapFrom != null && gapTo != null) {
132
+ startsInGap = gapFrom <= pos && pos <= gapTo;
133
+ endsInGap = gapFrom <= pos + node.nodeSize && pos + node.nodeSize <= gapTo;
134
+ }
135
+ // check if mark starts and ends in gap - skip mark if true
136
+ // for ReplaceStep marks startsInGap and endsInGap is always false
137
+ if (startsInGap && endsInGap) {
138
+ console.log("ignored mark - starts and ends in gap", {
139
+ startsInGap,
140
+ endsInGap,
141
+ markId: id,
142
+ markData,
143
+ node,
144
+ pos
145
+ });
146
+ return;
147
+ }
148
+ let startsInRange = false;
149
+ let endsInRange = false;
150
+ if (gapFrom != null && gapTo != null) {
151
+ // for ReplaceAround marks "startsInRange" means starts in [from, gapFrom] or [gapTo, to] ranges
152
+ // same for endsInRange
153
+ startsInRange = from <= pos && pos <= gapFrom || gapTo <= pos && pos <= to;
154
+ endsInRange = from <= pos + node.nodeSize && pos + node.nodeSize <= gapFrom || gapTo <= pos + node.nodeSize && pos + node.nodeSize <= to;
155
+ } else {
156
+ // for ReplaceStep marks "startsInRange" means starts in [from, to] range
157
+ // same for endsInRange
158
+ startsInRange = from <= pos && pos <= to;
159
+ endsInRange = from <= pos + node.nodeSize && pos + node.nodeSize <= to;
160
+ }
161
+ if (!startsInRange && !endsInRange) {
162
+ console.log("ignored mark - does not start or end in range", {
163
+ startsInRange,
164
+ endsInRange,
165
+ markId: id,
166
+ markData,
167
+ node,
168
+ pos
169
+ });
170
+ return;
171
+ }
172
+ console.log("found nested structure mark", {
173
+ mark,
174
+ markId: id,
175
+ markData,
176
+ startsInGap,
177
+ endsInGap,
178
+ startsInRange,
179
+ endsInRange,
180
+ node,
181
+ pos,
182
+ mainMark: {
183
+ markFrom,
184
+ markTo,
185
+ markGapFrom,
186
+ markGapTo
187
+ }
188
+ });
189
+ structureMarkGroups.add(id);
190
+ });
191
+ return true;
192
+ });
193
+ // revert structure mark groups in decreasing order of their ids
194
+ const markIds = Array.from(structureMarkGroups.values()).sort((a, b)=>Number(b) - Number(a));
195
+ markIds.forEach((id)=>{
196
+ const group = findStructureMarkGroupBySuggestionId(id, tr);
197
+ if (direction === "apply") {
198
+ applyStructureMarkGroup(group, tr);
199
+ } else {
200
+ revertStructureMarkGroup(group, tr);
201
+ }
202
+ });
203
+ console.groupEnd();
204
+ }
205
+ function getPosFromMark(mark, pos, node) {
206
+ const markData = mark.attrs["data"];
207
+ if (!markData) return null;
208
+ if (markData.position === "start") {
209
+ return pos;
210
+ }
211
+ if (markData.position === "end") {
212
+ return pos + node.nodeSize;
213
+ }
214
+ if (markData.position === "innerStart") {
215
+ return pos + 1;
216
+ }
217
+ if (markData.position === "innerEnd") {
218
+ return pos + node.nodeSize - 1;
219
+ }
220
+ return null;
221
+ }
222
+ export function applyStructureMarkGroup(group, tr) {
223
+ console.groupCollapsed("apply structure group, id = ", group.markFrom.mark.attrs["id"]);
224
+ console.log({
225
+ group
226
+ });
227
+ if (group.type === "replace") {
228
+ const { markFrom, markTo } = group;
229
+ tr.removeNodeMark(markFrom.pos, markFrom.mark);
230
+ tr.removeNodeMark(markTo.pos, markTo.mark);
231
+ console.groupEnd();
232
+ return;
233
+ }
234
+ const { markFrom, markTo, markGapFrom, markGapTo } = group;
235
+ tr.removeNodeMark(markFrom.pos, markFrom.mark);
236
+ tr.removeNodeMark(markTo.pos, markTo.mark);
237
+ tr.removeNodeMark(markGapFrom.pos, markGapFrom.mark);
238
+ tr.removeNodeMark(markGapTo.pos, markGapTo.mark);
239
+ console.groupEnd();
240
+ }
241
+ function revertStructureMarkGroup(group, tr) {
242
+ console.groupCollapsed("revert structure group, id = ", group.markFrom.mark.attrs["id"]);
243
+ console.log({
244
+ group
245
+ });
246
+ if (group.type === "replace") {
247
+ const { markFrom, markTo } = group;
248
+ // extract positions from, to, gapFrom and gapTo from marks
249
+ // the positions are either the mark's start, or the mark's end
250
+ const from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
251
+ const to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
252
+ if (from === null || to === null) {
253
+ throw new Error(`Could not find all positions for suggestion`);
254
+ }
255
+ // extract the rest of the data required to reconstruct the step: slice, and structure
256
+ // any of those 2 marks can be used for that, this data is identical in all of them
257
+ const mark = markFrom.mark;
258
+ const markData = mark.attrs["data"];
259
+ if (!markData) {
260
+ throw new Error(`Missing mark data for suggestion`);
261
+ }
262
+ const slice = markData.slice ? Slice.fromJSON(tr.doc.type.schema, markData.slice) : Slice.empty;
263
+ const isStepStructural = markData.structure ?? false;
264
+ // reconstruct the step
265
+ // this is the inverse step of the step that created this change
266
+ const step = new ReplaceStep(from, to, slice, isStepStructural);
267
+ console.log({
268
+ step
269
+ });
270
+ tr.removeNodeMark(markFrom.pos, markFrom.mark);
271
+ tr.removeNodeMark(markTo.pos, markTo.mark);
272
+ tr.step(step);
273
+ console.groupEnd();
274
+ return;
275
+ }
276
+ const { markFrom, markTo, markGapFrom, markGapTo } = group;
277
+ // extract positions from, to, gapFrom and gapTo from marks
278
+ // the positions are either the mark's start, or the mark's end
279
+ const from = getPosFromMark(markFrom.mark, markFrom.pos, markFrom.node);
280
+ const to = getPosFromMark(markTo.mark, markTo.pos, markTo.node);
281
+ const gapFrom = getPosFromMark(markGapFrom.mark, markGapFrom.pos, markGapFrom.node);
282
+ const gapTo = getPosFromMark(markGapTo.mark, markGapTo.pos, markGapTo.node);
283
+ if (from === null || to === null || gapFrom === null || gapTo === null) {
284
+ throw new Error(`Could not find all positions for suggestion`);
285
+ }
286
+ // extract the rest of the data required to reconstruct the step: insert, slice, and structure
287
+ // any of those 4 marks can be used for that, this data is identical in all of them
288
+ const mark = markGapFrom.mark;
289
+ const markData = mark.attrs["data"];
290
+ if (markData?.insert == null) {
291
+ throw new Error(`Missing insert for suggestion`);
292
+ }
293
+ const slice = markData.slice ? Slice.fromJSON(tr.doc.type.schema, markData.slice) : Slice.empty;
294
+ const insert = markData.insert;
295
+ const isStepStructural = markData.structure ?? false;
296
+ // reconstruct the step
297
+ // this is the inverse step of the step that created this change
298
+ const step = new ReplaceAroundStep(from, to, gapFrom, gapTo, slice, insert, isStepStructural);
299
+ console.log({
300
+ step
301
+ });
302
+ tr.removeNodeMark(markFrom.pos, markFrom.mark);
303
+ tr.removeNodeMark(markTo.pos, markTo.mark);
304
+ tr.removeNodeMark(markGapFrom.pos, markGapFrom.mark);
305
+ tr.removeNodeMark(markGapTo.pos, markGapTo.mark);
306
+ tr.step(step);
307
+ console.groupEnd();
308
+ }
309
+ function findStructureMarkGroupBySuggestionId(suggestionId, tr) {
310
+ const { structure } = getSuggestionMarks(tr.doc.type.schema);
311
+ let markFrom = null;
312
+ let markTo = null;
313
+ let markGapFrom = null;
314
+ let markGapTo = null;
315
+ // using suggestionId, find 4 marks: from, to, gapFrom and gapTo
316
+ tr.doc.nodesBetween(0, tr.doc.content.size, (node, pos)=>{
317
+ node.marks.forEach((mark)=>{
318
+ if (mark.type !== structure) return;
319
+ if (mark.attrs["id"] !== suggestionId) return;
320
+ const markData = mark.attrs["data"];
321
+ if (!markData) return;
322
+ if (markData.value === "from") {
323
+ markFrom = {
324
+ pos,
325
+ node,
326
+ mark
327
+ };
328
+ }
329
+ if (markData.value === "to") {
330
+ markTo = {
331
+ pos,
332
+ node,
333
+ mark
334
+ };
335
+ }
336
+ if (markData.value === "gapFrom") {
337
+ markGapFrom = {
338
+ pos,
339
+ node,
340
+ mark
341
+ };
342
+ }
343
+ if (markData.value === "gapTo") {
344
+ markGapTo = {
345
+ pos,
346
+ node,
347
+ mark
348
+ };
349
+ }
350
+ });
351
+ return markFrom == null || markTo == null || markGapFrom == null || markGapTo == null;
352
+ });
353
+ if (markFrom == null || markTo == null) {
354
+ throw new Error(`Could not find all marks for suggestion id ${suggestionId}`);
355
+ }
356
+ const type = markFrom.mark.attrs["data"]?.type;
357
+ if (type === "replace") {
358
+ return {
359
+ type: "replace",
360
+ markFrom,
361
+ markTo
362
+ };
363
+ }
364
+ if (markGapFrom == null || markGapTo == null) {
365
+ throw new Error(`Could not find all gap marks for replace around suggestion id ${suggestionId}`);
366
+ }
367
+ return {
368
+ type: "replaceAround",
369
+ markFrom,
370
+ markTo,
371
+ markGapFrom,
372
+ markGapTo
373
+ };
374
+ }
@@ -8,12 +8,12 @@ export function parseSuggestionId(id) {
8
8
  return parsed;
9
9
  }
10
10
  export function generateNextNumberId(schema, doc) {
11
- const { deletion, insertion, modification } = getSuggestionMarks(schema);
11
+ const { deletion, insertion, modification, structure } = getSuggestionMarks(schema);
12
12
  // Find the highest change id in the document so far,
13
13
  // and use that as the starting point for new changes
14
14
  let suggestionId = 0;
15
15
  doc?.descendants((node)=>{
16
- const mark = node.marks.find((mark)=>mark.type === insertion || mark.type === deletion || mark.type === modification);
16
+ const mark = node.marks.find((mark)=>mark.type === insertion || mark.type === deletion || mark.type === modification || mark.type === structure);
17
17
  if (mark) {
18
18
  suggestionId = Math.max(suggestionId, mark.attrs["id"]);
19
19
  return false;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { addSuggestionMarks, insertion, deletion, modification, hiddenDeletion, } from "./schema.js";
1
+ export { addSuggestionMarks, insertion, deletion, modification, hiddenDeletion, structure, } from "./schema.js";
2
2
  export { selectSuggestion, revertSuggestion, revertSuggestions, applySuggestion, applySuggestions, enableSuggestChanges, disableSuggestChanges, toggleSuggestChanges, } from "./commands.js";
3
3
  export { suggestChanges, suggestChangesKey, isSuggestChangesEnabled, } from "./plugin.js";
4
4
  export { withSuggestChanges, transformToSuggestionTransaction, } from "./withSuggestChanges.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { addSuggestionMarks, insertion, deletion, modification, hiddenDeletion } from "./schema.js";
1
+ export { addSuggestionMarks, insertion, deletion, modification, hiddenDeletion, structure } from "./schema.js";
2
2
  export { selectSuggestion, revertSuggestion, revertSuggestions, applySuggestion, applySuggestions, enableSuggestChanges, disableSuggestChanges, toggleSuggestChanges } from "./commands.js";
3
3
  export { suggestChanges, suggestChangesKey, isSuggestChangesEnabled } from "./plugin.js";
4
4
  export { withSuggestChanges, transformToSuggestionTransaction } from "./withSuggestChanges.js";
@@ -0,0 +1,9 @@
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;
@@ -0,0 +1,11 @@
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
+ }
@@ -6,6 +6,7 @@ import { suggestRemoveNodeMarkStep } from "./removeNodeMarkStep.js";
6
6
  import { suggestReplaceStep } from "./replaceStep.js";
7
7
  import { getSuggestionMarks } from "./utils.js";
8
8
  import { applySuggestionsToRange } from "./commands.js";
9
+ import { handleStructureStep } from "./features/wrapUnwrap/handleStructureStep.js";
9
10
  /**
10
11
  * This detects and handles changes from `setNodeMarkup` so that these are tracked as a modification
11
12
  * instead of a deletion + insertion
@@ -80,7 +81,11 @@ import { applySuggestionsToRange } from "./commands.js";
80
81
  * equivalent replace step will be generated, and then processed via
81
82
  * trackReplaceStep().
82
83
  */ export function suggestReplaceAroundStep(trackedTransaction, state, doc, step, prevSteps, suggestionId) {
83
- const handled = suggestSetNodeMarkup(trackedTransaction, state, doc, step, prevSteps, suggestionId);
84
+ let handled = handleStructureStep(trackedTransaction, step, prevSteps, suggestionId);
85
+ if (handled) {
86
+ return true;
87
+ }
88
+ handled = suggestSetNodeMarkup(trackedTransaction, state, doc, step, prevSteps, suggestionId);
84
89
  if (handled) {
85
90
  return true;
86
91
  }
@@ -4,6 +4,7 @@ import { rebasePos } from "./rebasePos.js";
4
4
  import { getSuggestionMarks } from "./utils.js";
5
5
  import { joinBlocks } from "./features/joinBlocks/index.js";
6
6
  import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSPDeletions } from "./features/joinOnDelete/index.js";
7
+ import { handleStructureStep } from "./features/wrapUnwrap/handleStructureStep.js";
7
8
  /**
8
9
  * Transform a replace step into its equivalent tracked steps.
9
10
  *
@@ -34,6 +35,10 @@ import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSP
34
35
  * will be joined and given the same ids. Any no-longer-necessary
35
36
  * zero-width spaces will be removed.
36
37
  */ export function suggestReplaceStep(trackedTransaction, state, doc, step, prevSteps, suggestionId) {
38
+ const handled = handleStructureStep(trackedTransaction, step, prevSteps, suggestionId);
39
+ if (handled) {
40
+ return true;
41
+ }
37
42
  const { deletion, insertion } = getSuggestionMarks(state.schema);
38
43
  // Check for insertion and deletion marks directly
39
44
  // adjacent to this step's boundaries. If they exist,
package/dist/schema.d.ts CHANGED
@@ -3,10 +3,11 @@ export declare const deletion: MarkSpec;
3
3
  export declare const hiddenDeletion: MarkSpec;
4
4
  export declare const insertion: MarkSpec;
5
5
  export declare const modification: MarkSpec;
6
+ export declare const structure: MarkSpec;
6
7
  /**
7
8
  * Add the deletion, insertion, and modification marks to
8
9
  * the provided MarkSpec map.
9
10
  */
10
11
  export declare function addSuggestionMarks<Marks extends string>(marks: Record<Marks, MarkSpec>, opts?: {
11
12
  experimental_deletions?: "hidden" | "visible";
12
- }): Record<Marks | "deletion" | "insertion" | "modification", MarkSpec>;
13
+ }): Record<Marks | "deletion" | "insertion" | "modification" | "structure", MarkSpec>;
package/dist/schema.js CHANGED
@@ -166,6 +166,41 @@ export const modification = {
166
166
  }
167
167
  ]
168
168
  };
169
+ export const structure = {
170
+ inclusive: false,
171
+ excludes: "deletion insertion modification",
172
+ attrs: {
173
+ id: {
174
+ validate: suggestionIdValidate
175
+ },
176
+ data: {
177
+ default: null
178
+ }
179
+ },
180
+ toDOM (mark) {
181
+ return [
182
+ "div",
183
+ {
184
+ "data-type": "structure",
185
+ "data-id": JSON.stringify(mark.attrs["id"]),
186
+ "data-data": JSON.stringify(mark.attrs["data"])
187
+ },
188
+ 0
189
+ ];
190
+ },
191
+ parseDOM: [
192
+ {
193
+ tag: "div[data-type='structure']",
194
+ getAttrs (node) {
195
+ if (!node.dataset["id"]) return false;
196
+ return {
197
+ id: JSON.parse(node.dataset["id"]),
198
+ data: JSON.parse(node.dataset["data"] ?? "null")
199
+ };
200
+ }
201
+ }
202
+ ]
203
+ };
169
204
  /**
170
205
  * Add the deletion, insertion, and modification marks to
171
206
  * the provided MarkSpec map.
@@ -174,6 +209,7 @@ export const modification = {
174
209
  ...marks,
175
210
  deletion: opts?.experimental_deletions === "hidden" ? hiddenDeletion : deletion,
176
211
  insertion,
177
- modification
212
+ modification,
213
+ structure
178
214
  };
179
215
  }
@@ -1,6 +1,6 @@
1
1
  import { Schema, type Node } from "prosemirror-model";
2
2
  import { type MarkBuilder, type NodeBuilder } from "prosemirror-test-builder";
3
- declare const schema: Schema<"blockquote" | "text" | "doc" | "paragraph" | "image" | "orderedList" | "bulletList" | "listItem" | "horizontal_rule" | "heading" | "code_block" | "hard_break", "insertion" | "deletion" | "modification" | "code" | "em" | "link" | "strong" | "difficulty">;
3
+ export declare const schema: Schema<"blockquote" | "text" | "doc" | "paragraph" | "bulletList" | "listItem" | "orderedList" | "image" | "horizontal_rule" | "heading" | "code_block" | "hard_break", "insertion" | "deletion" | "modification" | "structure" | "code" | "em" | "link" | "strong" | "difficulty">;
4
4
  export declare const testBuilders: { [NodeTypeName in keyof (typeof schema)["nodes"]]: NodeBuilder; } & { [MarkTypeName in keyof (typeof schema)["marks"]]: MarkBuilder; } & {
5
5
  schema: typeof schema;
6
6
  };
@@ -8,4 +8,3 @@ export type TaggedNode = Node & {
8
8
  flat: Node;
9
9
  tag: Record<string, number>;
10
10
  };
11
- export {};
package/dist/utils.d.ts CHANGED
@@ -3,6 +3,7 @@ export interface SuggestionMarks {
3
3
  insertion: MarkType;
4
4
  deletion: MarkType;
5
5
  modification: MarkType;
6
+ structure: MarkType;
6
7
  }
7
8
  /**
8
9
  * Get the suggestion mark types from a schema, with proper error handling.
package/dist/utils.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Get the suggestion mark types from a schema, with proper error handling.
3
3
  * Throws an error if any of the required marks are not found.
4
4
  */ export function getSuggestionMarks(schema) {
5
- const { insertion, deletion, modification } = schema.marks;
5
+ const { insertion, deletion, modification, structure } = schema.marks;
6
6
  if (!insertion) {
7
7
  throw new Error("Failed to find insertion mark in schema. Did you forget to add it?");
8
8
  }
@@ -12,9 +12,13 @@
12
12
  if (!modification) {
13
13
  throw new Error("Failed to find modification mark in schema. Did you forget to add it?");
14
14
  }
15
+ if (!structure) {
16
+ throw new Error("Failed to find structure mark in schema. Did you forget to add it?");
17
+ }
15
18
  return {
16
19
  insertion,
17
20
  deletion,
18
- modification
21
+ modification,
22
+ structure
19
23
  };
20
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-marker/prosemirror-suggest-changes",
3
- "version": "0.3.1",
3
+ "version": "0.3.3-wrap-unwrap.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",