@magic-marker/prosemirror-suggest-changes 0.1.8

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/dist/addMarkStep.d.ts +12 -0
  4. package/dist/addMarkStep.js +17 -0
  5. package/dist/addNodeMarkStep.d.ts +11 -0
  6. package/dist/addNodeMarkStep.js +36 -0
  7. package/dist/attrStep.d.ts +11 -0
  8. package/dist/attrStep.js +33 -0
  9. package/dist/commands.d.ts +64 -0
  10. package/dist/commands.js +314 -0
  11. package/dist/decorations.d.ts +3 -0
  12. package/dist/decorations.js +73 -0
  13. package/dist/features/joinBlocks/__tests__/blockJoin.playwright.test.d.ts +14 -0
  14. package/dist/features/joinBlocks/__tests__/crossBlockReplace.test.d.ts +1 -0
  15. package/dist/features/joinBlocks/__tests__/getZWSPPairsInRange.test.d.ts +1 -0
  16. package/dist/features/joinBlocks/__tests__/multiStepBlockJoin.test.d.ts +1 -0
  17. package/dist/features/joinBlocks/__tests__/nestedBlockJoin.test.d.ts +1 -0
  18. package/dist/features/joinBlocks/__tests__/paragraphBackspace.test.d.ts +1 -0
  19. package/dist/features/joinBlocks/__tests__/playwrightHelpers.d.ts +143 -0
  20. package/dist/features/joinBlocks/__tests__/testHelpers.d.ts +106 -0
  21. package/dist/features/joinBlocks/index.d.ts +3 -0
  22. package/dist/features/joinBlocks/index.js +64 -0
  23. package/dist/features/joinBlocks/types.d.ts +11 -0
  24. package/dist/features/joinBlocks/types.js +1 -0
  25. package/dist/features/joinBlocks/utils/boundary.d.ts +10 -0
  26. package/dist/features/joinBlocks/utils/boundary.js +147 -0
  27. package/dist/features/joinBlocks/utils/getZWSPPairsInRange.d.ts +10 -0
  28. package/dist/features/joinBlocks/utils/getZWSPPairsInRange.js +57 -0
  29. package/dist/findSuggestionMarkEnd.d.ts +2 -0
  30. package/dist/findSuggestionMarkEnd.js +38 -0
  31. package/dist/generateId.d.ts +5 -0
  32. package/dist/generateId.js +24 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.js +4 -0
  35. package/dist/plugin.d.ts +8 -0
  36. package/dist/plugin.js +38 -0
  37. package/dist/rebasePos.d.ts +9 -0
  38. package/dist/rebasePos.js +10 -0
  39. package/dist/removeMarkStep.d.ts +12 -0
  40. package/dist/removeMarkStep.js +17 -0
  41. package/dist/removeNodeMarkStep.d.ts +11 -0
  42. package/dist/removeNodeMarkStep.js +30 -0
  43. package/dist/replaceAroundStep.d.ts +12 -0
  44. package/dist/replaceAroundStep.js +96 -0
  45. package/dist/replaceStep.d.ts +35 -0
  46. package/dist/replaceStep.js +247 -0
  47. package/dist/schema.d.ts +9 -0
  48. package/dist/schema.js +139 -0
  49. package/dist/testing/difficultyMark.d.ts +2 -0
  50. package/dist/testing/testBuilders.d.ts +11 -0
  51. package/dist/utils.d.ts +11 -0
  52. package/dist/utils.js +20 -0
  53. package/dist/withSuggestChanges.d.ts +27 -0
  54. package/dist/withSuggestChanges.js +114 -0
  55. package/package.json +83 -0
@@ -0,0 +1,314 @@
1
+ import { TextSelection } from "prosemirror-state";
2
+ import { Transform } from "prosemirror-transform";
3
+ import { findSuggestionMarkEnd } from "./findSuggestionMarkEnd.js";
4
+ import { suggestChangesKey } from "./plugin.js";
5
+ import { getSuggestionMarks } from "./utils.js";
6
+ /**
7
+ * Given a node and a transform, add a set of steps to the
8
+ * transform that applies all marks of type markTypeToApply
9
+ * and reverts all marks of type markTypeToRevert.
10
+ *
11
+ * If suggestionId is provided, will only add steps that impact
12
+ * deletions, insertions, and modifications with that id.
13
+ */ function applySuggestionsToTransform(node, tr, markTypeToApply, markTypeToRevert, suggestionId, from, to) {
14
+ const toApplyIsInSet = suggestionId === undefined ? (marks)=>markTypeToApply.isInSet(marks) : (marks)=>{
15
+ const mark = markTypeToApply.isInSet(marks);
16
+ return mark && mark.attrs["id"] === suggestionId ? mark : undefined;
17
+ };
18
+ const toRevertIsInSet = suggestionId === undefined ? (marks)=>markTypeToRevert.isInSet(marks) : (marks)=>{
19
+ const mark = markTypeToRevert.isInSet(marks);
20
+ return mark && mark.attrs["id"] === suggestionId ? mark : undefined;
21
+ };
22
+ const isToApply = toApplyIsInSet(node.marks);
23
+ if (isToApply) {
24
+ if (node.isInline) {
25
+ tr.removeMark(0, node.nodeSize, markTypeToApply);
26
+ } else {
27
+ tr.removeNodeMark(0, markTypeToApply);
28
+ }
29
+ }
30
+ node.descendants((child, pos)=>{
31
+ if (from !== undefined && pos < from) {
32
+ return true;
33
+ }
34
+ if (to !== undefined && pos > to) {
35
+ return false;
36
+ }
37
+ const isToRevert = toRevertIsInSet(child.marks);
38
+ const isToApply = toApplyIsInSet(child.marks);
39
+ if (!isToRevert && !isToApply) {
40
+ return true;
41
+ }
42
+ if (isToRevert) {
43
+ const { pos: deletionFrom, deleted } = tr.mapping.mapResult(pos);
44
+ if (deleted) return false;
45
+ const deletionTo = findSuggestionMarkEnd(tr.doc.resolve(deletionFrom + child.nodeSize), markTypeToRevert);
46
+ // check if the previous and the next text part is a space
47
+ // if so, we can delete the whole text part
48
+ const prevChar = tr.doc.textBetween(deletionFrom - 1, deletionFrom, "x", "x");
49
+ const nextChar = // textBetween is fine with negative positions (??),
50
+ // but it errors if passed a position greater than the
51
+ // size of the doc
52
+ deletionTo <= tr.doc.content.size ? tr.doc.textBetween(deletionTo, deletionTo + 1, "x", "x") : "";
53
+ const addedRange = prevChar === " " && nextChar === " " ? 1 : 0;
54
+ tr.deleteRange(deletionFrom, deletionTo + addedRange);
55
+ return false;
56
+ }
57
+ const insertionFrom = tr.mapping.map(pos);
58
+ const insertionTo = insertionFrom + child.nodeSize;
59
+ if (child.isInline) {
60
+ tr.removeMark(insertionFrom, insertionTo, markTypeToApply);
61
+ if (child.text === "\u200B") {
62
+ tr.delete(insertionFrom, insertionTo);
63
+ }
64
+ } else {
65
+ tr.removeNodeMark(insertionFrom, markTypeToApply);
66
+ }
67
+ return true;
68
+ });
69
+ }
70
+ function revertModifications(node, pos, tr) {
71
+ const { modification } = getSuggestionMarks(node.type.schema);
72
+ const existingMods = node.marks.filter((mark)=>mark.type === modification);
73
+ for (const mod of existingMods){
74
+ if (mod.attrs["type"] === "attr" && typeof mod.attrs["attrName"] === "string") {
75
+ tr.setNodeAttribute(pos, mod.attrs["attrName"], mod.attrs["previousValue"]);
76
+ } else if (mod.attrs["type"] === "mark") {
77
+ if (mod.attrs["previousValue"]) {
78
+ tr.addNodeMark(0, node.type.schema.markFromJSON(mod.attrs["previousValue"]));
79
+ } else {
80
+ tr.removeNodeMark(pos, node.type.schema.markFromJSON(mod.attrs["previousValue"]));
81
+ }
82
+ } else if (mod.attrs["type"] === "nodeType") {
83
+ tr.setNodeMarkup(pos, node.type.schema.nodes[mod.attrs["previousValue"]], null);
84
+ } else {
85
+ throw new Error("Unknown modification type");
86
+ }
87
+ }
88
+ }
89
+ function modificationIsInSet(modification, id, marks) {
90
+ const mark = modification.isInSet(marks);
91
+ if (id === undefined) return mark;
92
+ if (mark?.attrs["id"] === id) return mark;
93
+ return undefined;
94
+ }
95
+ function applyModificationsToTransform(node, tr, dir, suggestionId, from, to) {
96
+ const { modification } = getSuggestionMarks(node.type.schema);
97
+ const isModification = modificationIsInSet(modification, suggestionId, node.marks);
98
+ if (isModification) {
99
+ let prevLength;
100
+ do {
101
+ // https://github.com/ProseMirror/prosemirror/issues/1525
102
+ prevLength = tr.steps.length;
103
+ tr.removeNodeMark(0, modification);
104
+ }while (tr.steps.length > prevLength);
105
+ if (dir < 0) {
106
+ revertModifications(node, 0, tr);
107
+ }
108
+ }
109
+ node.descendants((child, pos)=>{
110
+ if (from !== undefined && pos < from) {
111
+ return true;
112
+ }
113
+ if (to !== undefined && pos > to) {
114
+ return false;
115
+ }
116
+ const isModification = modificationIsInSet(modification, suggestionId, child.marks);
117
+ if (!isModification) {
118
+ return true;
119
+ }
120
+ let prevLength;
121
+ do {
122
+ // https://github.com/ProseMirror/prosemirror/issues/1525
123
+ prevLength = tr.steps.length;
124
+ tr.removeNodeMark(pos, modification);
125
+ }while (tr.steps.length > prevLength);
126
+ if (dir < 0) {
127
+ revertModifications(child, pos, tr);
128
+ }
129
+ return true;
130
+ });
131
+ }
132
+ export function applySuggestionsToNode(node) {
133
+ const { deletion, insertion } = getSuggestionMarks(node.type.schema);
134
+ const transform = new Transform(node);
135
+ applySuggestionsToTransform(node, transform, insertion, deletion);
136
+ applyModificationsToTransform(node, transform, 1);
137
+ return transform.doc;
138
+ }
139
+ export function applySuggestionsToRange(doc, from, to) {
140
+ // blockRange can only return null if a predicate is provided
141
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
142
+ const nodeRange = doc.resolve(from).blockRange(doc.resolve(to));
143
+ const { deletion, insertion } = getSuggestionMarks(doc.type.schema);
144
+ const transform = new Transform(doc);
145
+ applySuggestionsToTransform(doc, transform, insertion, deletion, undefined, nodeRange.start, nodeRange.end);
146
+ applyModificationsToTransform(doc, transform, 1, undefined, nodeRange.start, nodeRange.end);
147
+ return transform.doc.slice(transform.mapping.map(from), transform.mapping.map(to));
148
+ }
149
+ /**
150
+ * Command that applies all tracked changes in a document.
151
+ *
152
+ * This means that all content within deletion marks will be deleted.
153
+ * Insertion marks and modification marks will be removed, and their
154
+ * contents left in the doc.
155
+ */ export function applySuggestions(state, dispatch) {
156
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
157
+ const tr = state.tr;
158
+ applySuggestionsToTransform(state.doc, tr, insertion, deletion);
159
+ applyModificationsToTransform(tr.doc, tr, 1);
160
+ tr.setMeta(suggestChangesKey, {
161
+ skip: true
162
+ });
163
+ dispatch?.(tr);
164
+ return true;
165
+ }
166
+ /**
167
+ * Command that applies all tracked changes in specified range.
168
+ *
169
+ * This means that all content within deletion marks will be deleted.
170
+ * Insertion marks and modification marks will be removed, and their
171
+ * contents left in the doc.
172
+ */ export function applySuggestionsInRange(from, to) {
173
+ return (state, dispatch)=>{
174
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
175
+ const tr = state.tr;
176
+ applySuggestionsToTransform(state.doc, tr, insertion, deletion, undefined, from, to);
177
+ applyModificationsToTransform(tr.doc, tr, 1, undefined, from, to);
178
+ tr.setMeta(suggestChangesKey, {
179
+ skip: true
180
+ });
181
+ dispatch?.(tr);
182
+ return true;
183
+ };
184
+ }
185
+ /**
186
+ * Command that applies a given tracked change to a document.
187
+ *
188
+ * This means that all content within the deletion mark will be deleted.
189
+ * The insertion mark and modification mark will be removed, and their
190
+ * contents left in the doc.
191
+ */ export function applySuggestion(suggestionId, from, to) {
192
+ return (state, dispatch)=>{
193
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
194
+ const tr = state.tr;
195
+ applySuggestionsToTransform(state.doc, tr, insertion, deletion, suggestionId, from, to);
196
+ applyModificationsToTransform(tr.doc, tr, 1, undefined, from, to);
197
+ if (!tr.steps.length) return false;
198
+ tr.setMeta(suggestChangesKey, {
199
+ skip: true
200
+ });
201
+ dispatch?.(tr);
202
+ return true;
203
+ };
204
+ }
205
+ /**
206
+ * Command that reverts all tracked changes in a document.
207
+ *
208
+ * This means that all content within insertion marks will be deleted.
209
+ * Deletion marks will be removed, and their contents left in the doc.
210
+ * Modifications tracked in modification marks will be reverted.
211
+ */ export function revertSuggestions(state, dispatch) {
212
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
213
+ const tr = state.tr;
214
+ applySuggestionsToTransform(state.doc, tr, deletion, insertion);
215
+ applyModificationsToTransform(tr.doc, tr, -1);
216
+ tr.setMeta(suggestChangesKey, {
217
+ skip: true
218
+ });
219
+ dispatch?.(tr);
220
+ return true;
221
+ }
222
+ /**
223
+ * Command that reverts all tracked changes in specified range.
224
+ *
225
+ * This means that all content within insertion marks will be deleted.
226
+ * Deletion marks will be removed, and their contents left in the doc.
227
+ * Modifications tracked in modification marks will be reverted.
228
+ */ export function revertSuggestionsInRange(from, to) {
229
+ return (state, dispatch)=>{
230
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
231
+ const tr = state.tr;
232
+ applySuggestionsToTransform(state.doc, tr, deletion, insertion, undefined, from, to);
233
+ applyModificationsToTransform(tr.doc, tr, -1, undefined, from, to);
234
+ tr.setMeta(suggestChangesKey, {
235
+ skip: true
236
+ });
237
+ dispatch?.(tr);
238
+ return true;
239
+ };
240
+ }
241
+ /**
242
+ * Command that reverts a given tracked change in a document.
243
+ *
244
+ * This means that all content within the insertion mark will be deleted.
245
+ * The deletion mark will be removed, and their contents left in the doc.
246
+ * Modifications tracked in modification marks will be reverted.
247
+ */ export function revertSuggestion(suggestionId, from, to) {
248
+ return (state, dispatch)=>{
249
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
250
+ const tr = state.tr;
251
+ applySuggestionsToTransform(state.doc, tr, deletion, insertion, suggestionId, from, to);
252
+ if (!tr.steps.length) return false;
253
+ tr.setMeta(suggestChangesKey, {
254
+ skip: true
255
+ });
256
+ applyModificationsToTransform(tr.doc, tr, -1, undefined, from, to);
257
+ dispatch?.(tr);
258
+ return true;
259
+ };
260
+ }
261
+ /**
262
+ * Command that updates the selection to cover an existing change.
263
+ */ export function selectSuggestion(suggestionId) {
264
+ return (state, dispatch)=>{
265
+ const { deletion, insertion, modification } = getSuggestionMarks(state.schema);
266
+ let changeStart = null;
267
+ let changeEnd = null;
268
+ state.doc.descendants((node, pos)=>{
269
+ const mark = node.marks.find((mark)=>mark.type === insertion || mark.type === deletion || mark.type === modification);
270
+ if (mark?.attrs["id"] !== suggestionId) return true;
271
+ if (changeStart === null) {
272
+ changeStart = pos;
273
+ changeEnd = pos + node.nodeSize;
274
+ return false;
275
+ }
276
+ changeEnd = pos + node.nodeSize;
277
+ return false;
278
+ });
279
+ if (changeStart === null || changeEnd === null) {
280
+ return false;
281
+ }
282
+ if (!dispatch) return true;
283
+ dispatch(state.tr.setSelection(TextSelection.create(state.doc, changeStart, changeEnd)).scrollIntoView());
284
+ return true;
285
+ };
286
+ }
287
+ /** Command that enables suggest changes */ export function enableSuggestChanges(state, dispatch) {
288
+ if (!suggestChangesKey.getState(state)) return false;
289
+ if (!dispatch) return true;
290
+ dispatch(state.tr.setMeta(suggestChangesKey, {
291
+ skip: true,
292
+ enabled: true
293
+ }));
294
+ return true;
295
+ }
296
+ /** Command that disables suggest changes */ export function disableSuggestChanges(state, dispatch) {
297
+ if (!suggestChangesKey.getState(state)) return false;
298
+ if (!dispatch) return true;
299
+ dispatch(state.tr.setMeta(suggestChangesKey, {
300
+ skip: true,
301
+ enabled: false
302
+ }));
303
+ return true;
304
+ }
305
+ /** Command that toggles suggest changes on or off */ export function toggleSuggestChanges(state, dispatch) {
306
+ const pluginState = suggestChangesKey.getState(state);
307
+ if (!pluginState) return false;
308
+ if (!dispatch) return true;
309
+ dispatch(state.tr.setMeta(suggestChangesKey, {
310
+ skip: true,
311
+ enabled: !pluginState.enabled
312
+ }));
313
+ return true;
314
+ }
@@ -0,0 +1,3 @@
1
+ import type { EditorState } from "prosemirror-state";
2
+ import { type DecorationSource } from "prosemirror-view";
3
+ export declare function getSuggestionDecorations(state: EditorState): DecorationSource;
@@ -0,0 +1,73 @@
1
+ import { Decoration, DecorationSet } from "prosemirror-view";
2
+ import { getSuggestionMarks } from "./utils.js";
3
+ function pilcrow() {
4
+ const span = document.createElement("span");
5
+ span.appendChild(document.createTextNode("¶"));
6
+ return span;
7
+ }
8
+ export function getSuggestionDecorations(state) {
9
+ const { deletion, insertion } = getSuggestionMarks(state.schema);
10
+ const changeDecorations = [];
11
+ let lastParentNode = null;
12
+ let lastTextNode = null;
13
+ let lastTextNodeEndPos = 0;
14
+ state.doc.descendants((node, pos, parent)=>{
15
+ if (node.isTextblock && node.childCount) {
16
+ if (node.children.every((child)=>deletion.isInSet(child.marks))) {
17
+ changeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
18
+ "data-node-deletion": "true"
19
+ }));
20
+ }
21
+ if (node.children.every((child)=>insertion.isInSet(child.marks))) {
22
+ changeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
23
+ "data-node-insertion": "true"
24
+ }));
25
+ }
26
+ }
27
+ if (node.type.name !== "text") return true;
28
+ const currentDeletionMark = node.marks.find((mark)=>mark.type === deletion);
29
+ const currentInsertionMark = node.marks.find((mark)=>mark.type === insertion);
30
+ const lastDeletionMark = lastTextNode?.marks.find((mark)=>mark.type === deletion);
31
+ const lastInsertionMark = lastTextNode?.marks.find((mark)=>mark.type === insertion);
32
+ const widgetPos = lastTextNodeEndPos;
33
+ lastTextNode = node;
34
+ lastTextNodeEndPos = pos + node.nodeSize;
35
+ if (parent === lastParentNode) {
36
+ lastParentNode = parent;
37
+ return true;
38
+ }
39
+ lastParentNode = parent;
40
+ if ((!currentDeletionMark || !lastDeletionMark) && (!currentInsertionMark || !lastInsertionMark)) {
41
+ return true;
42
+ }
43
+ if (currentDeletionMark?.attrs["id"] !== lastDeletionMark?.attrs["id"] && currentInsertionMark?.attrs["id"] !== lastInsertionMark?.attrs["id"]) {
44
+ return true;
45
+ }
46
+ if (currentDeletionMark) {
47
+ changeDecorations.push(Decoration.widget(widgetPos, pilcrow, {
48
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
49
+ key: currentDeletionMark.attrs["id"],
50
+ marks: [
51
+ deletion.create({
52
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
53
+ id: currentDeletionMark.attrs["id"]
54
+ })
55
+ ]
56
+ }));
57
+ }
58
+ if (currentInsertionMark) {
59
+ changeDecorations.push(Decoration.widget(widgetPos, pilcrow, {
60
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
61
+ key: currentInsertionMark.attrs["id"],
62
+ marks: [
63
+ insertion.create({
64
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
65
+ id: currentInsertionMark.attrs["id"]
66
+ })
67
+ ]
68
+ }));
69
+ }
70
+ return true;
71
+ });
72
+ return DecorationSet.create(state.doc, changeDecorations);
73
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Playwright E2E Tests for Block Join Behavior
3
+ *
4
+ * These tests use real keyboard events in a browser environment to verify
5
+ * that the suggestChanges plugin correctly handles block splitting and joining
6
+ * operations.
7
+ *
8
+ * Key differences from JSDOM tests:
9
+ * - Uses real browser (via Playwright)
10
+ * - Uses real keyboard events (page.keyboard.press)
11
+ * - Tests actual user interactions
12
+ * - Catches bugs that manual transactions would hide
13
+ */
14
+ export {};
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Helper functions for Playwright E2E tests.
3
+ * These helpers reduce test duplication while keeping actual keyboard events.
4
+ */
5
+ import type { Page } from "@playwright/test";
6
+ export interface EditorState {
7
+ paragraphCount: number;
8
+ textContent: string;
9
+ cursorFrom: number;
10
+ cursorTo: number;
11
+ blockCount?: number;
12
+ }
13
+ /**
14
+ * Common test pattern: Enter then Backspace
15
+ */
16
+ export declare function testEnterThenBackspace(page: Page): Promise<{
17
+ initialState: {
18
+ paragraphCount: number;
19
+ blockCount: number;
20
+ textContent: string;
21
+ cursorFrom: number;
22
+ cursorTo: number;
23
+ };
24
+ afterEnterState: {
25
+ paragraphCount: number;
26
+ blockCount: number;
27
+ textContent: string;
28
+ cursorFrom: number;
29
+ cursorTo: number;
30
+ };
31
+ finalState: {
32
+ paragraphCount: number;
33
+ blockCount: number;
34
+ textContent: string;
35
+ cursorFrom: number;
36
+ cursorTo: number;
37
+ };
38
+ }>;
39
+ /**
40
+ * Common test pattern: Enter twice then Backspace twice
41
+ */
42
+ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
43
+ initialState: {
44
+ paragraphCount: number;
45
+ blockCount: number;
46
+ textContent: string;
47
+ cursorFrom: number;
48
+ cursorTo: number;
49
+ };
50
+ afterFirstEnter: {
51
+ paragraphCount: number;
52
+ blockCount: number;
53
+ textContent: string;
54
+ cursorFrom: number;
55
+ cursorTo: number;
56
+ };
57
+ afterSecondEnter: {
58
+ paragraphCount: number;
59
+ blockCount: number;
60
+ textContent: string;
61
+ cursorFrom: number;
62
+ cursorTo: number;
63
+ };
64
+ afterFirstBackspace: {
65
+ paragraphCount: number;
66
+ blockCount: number;
67
+ textContent: string;
68
+ cursorFrom: number;
69
+ cursorTo: number;
70
+ };
71
+ finalState: {
72
+ paragraphCount: number;
73
+ blockCount: number;
74
+ textContent: string;
75
+ cursorFrom: number;
76
+ cursorTo: number;
77
+ };
78
+ }>;
79
+ /**
80
+ * Common test pattern: Enter then Delete from first block
81
+ */
82
+ export declare function testEnterThenDeleteFromFirst(page: Page): Promise<{
83
+ initialState: {
84
+ paragraphCount: number;
85
+ blockCount: number;
86
+ textContent: string;
87
+ cursorFrom: number;
88
+ cursorTo: number;
89
+ };
90
+ afterEnter: {
91
+ paragraphCount: number;
92
+ blockCount: number;
93
+ textContent: string;
94
+ cursorFrom: number;
95
+ cursorTo: number;
96
+ };
97
+ finalState: {
98
+ paragraphCount: number;
99
+ blockCount: number;
100
+ textContent: string;
101
+ cursorFrom: number;
102
+ cursorTo: number;
103
+ };
104
+ }>;
105
+ /**
106
+ * Helper to get current editor state
107
+ */
108
+ export declare function getEditorState(page: Page): Promise<EditorState>;
109
+ /**
110
+ * Helper to perform Enter at position
111
+ */
112
+ export declare function pressEnterAt(page: Page, position: number): Promise<void>;
113
+ /**
114
+ * Helper to perform Backspace
115
+ */
116
+ export declare function pressBackspace(page: Page): Promise<void>;
117
+ /**
118
+ * Helper to perform Delete
119
+ */
120
+ export declare function pressDelete(page: Page): Promise<void>;
121
+ /**
122
+ * Common assertion: verify document reverted to original
123
+ */
124
+ export declare function assertReverted(finalState: EditorState, initialState: EditorState): void;
125
+ /**
126
+ * Setup document from JSON and get initial state.
127
+ * This helper replaces the editor document with the provided JSON,
128
+ * clears transactions, and returns initial state.
129
+ */
130
+ export declare function setupDocFromJSON(page: Page, docJSON: unknown): Promise<{
131
+ initialState: EditorState;
132
+ initialDoc: unknown;
133
+ }>;
134
+ /**
135
+ * Assert that document fully reverted to initial state.
136
+ * Combines text content and JSON structure assertions.
137
+ */
138
+ export declare function assertDocFullyReverted(finalState: EditorState, finalDoc: unknown, initialState: EditorState, initialDoc: unknown): void;
139
+ /**
140
+ * Perform a single Enter/Backspace cycle.
141
+ * Useful for testing multiple rapid sequences.
142
+ */
143
+ export declare function performEnterBackspaceCycle(page: Page): Promise<void>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Test helper functions for creating pre-split documents with ZWSPs.
3
+ * These helpers create documents that are already in a split state,
4
+ * eliminating the need for tests to programmatically create splits.
5
+ */
6
+ import { EditorState } from "prosemirror-state";
7
+ import type { MarkType } from "prosemirror-model";
8
+ import { type TaggedNode } from "../../../testing/testBuilders.js";
9
+ export interface SplitDocResult {
10
+ doc: TaggedNode;
11
+ state: EditorState;
12
+ insertion: MarkType;
13
+ }
14
+ /**
15
+ * Creates two paragraphs with a ZWSP marker at the boundary between them.
16
+ * This simulates the state after an Enter key press at the end of the first paragraph.
17
+ *
18
+ * @param text1 - Text for the first paragraph
19
+ * @param text2 - Text for the second paragraph (optional, defaults to empty)
20
+ * @param suggestionId - The suggestion ID for the ZWSPs and split content
21
+ * @returns Object containing the document, editor state, and insertion mark type
22
+ */
23
+ export declare const createSplitParagraphs: (text1: string, text2?: string, suggestionId?: number) => {
24
+ doc: import("prosemirror-model").Node & {
25
+ tag: {
26
+ [tag: string]: number;
27
+ };
28
+ };
29
+ state: EditorState;
30
+ insertion: MarkType;
31
+ };
32
+ /**
33
+ * Creates a list (bullet or ordered) with two list items and ZWSP markers at the boundary.
34
+ * This simulates the state after an Enter key press at the end of the first list item.
35
+ *
36
+ * @param listType - Type of list to create ('bullet' or 'ordered')
37
+ * @param item1Text - Text for the first list item
38
+ * @param item2Text - Text for the second list item (optional, defaults to empty)
39
+ * @param suggestionId - The suggestion ID for the ZWSPs and split content
40
+ * @returns Object containing the document, editor state, and insertion mark type
41
+ */
42
+ export declare const createSplitList: (listType: "bullet" | "ordered", item1Text: string, item2Text?: string, suggestionId?: number) => {
43
+ doc: import("prosemirror-model").Node & {
44
+ tag: {
45
+ [tag: string]: number;
46
+ };
47
+ };
48
+ state: EditorState;
49
+ insertion: MarkType;
50
+ };
51
+ /**
52
+ * Creates a bullet list with three list items after two sequential splits.
53
+ * This simulates pressing Enter twice, creating three list items with all ZWSPs in place.
54
+ *
55
+ * @param item1Text - Text for the first list item
56
+ * @param suggestionId - The suggestion ID for all ZWSPs
57
+ * @returns Object containing the document, editor state, and insertion mark type
58
+ */
59
+ export declare const createDoubleSplitBulletList: (item1Text: string, suggestionId?: number) => {
60
+ doc: import("prosemirror-model").Node & {
61
+ tag: {
62
+ [tag: string]: number;
63
+ };
64
+ };
65
+ state: EditorState;
66
+ insertion: MarkType;
67
+ };
68
+ /**
69
+ * Creates three paragraphs after two sequential splits.
70
+ * This simulates pressing Enter twice, creating three paragraphs with all ZWSPs in place.
71
+ *
72
+ * @param text1 - Text for the first paragraph
73
+ * @param suggestionId - The suggestion ID for all ZWSPs
74
+ * @returns Object containing the document, editor state, and insertion mark type
75
+ */
76
+ export declare const createDoubleSplitParagraphs: (text1: string, suggestionId?: number) => {
77
+ doc: import("prosemirror-model").Node & {
78
+ tag: {
79
+ [tag: string]: number;
80
+ };
81
+ };
82
+ state: EditorState;
83
+ insertion: MarkType;
84
+ };
85
+ /**
86
+ * Applies a backspace step at a tagged position in the document.
87
+ * This helper eliminates boilerplate for the common pattern of backspacing at a ZWSP marker.
88
+ *
89
+ * @param doc - The document with a tag marking the backspace position
90
+ * @param state - The editor state
91
+ * @param tagName - The tag name to backspace at (defaults to "zwsp")
92
+ * @param suggestionId - The suggestion ID for this operation
93
+ * @returns The new editor state after applying the backspace
94
+ */
95
+ export declare const applyBackspaceAtTag: (doc: TaggedNode, state: EditorState, tagName?: string, suggestionId?: number) => EditorState;
96
+ /**
97
+ * Applies a forward delete step at a tagged position in the document.
98
+ * This helper eliminates boilerplate for the common pattern of deleting at a position.
99
+ *
100
+ * @param doc - The document with a tag marking the delete position
101
+ * @param state - The editor state
102
+ * @param tagName - The tag name to delete at
103
+ * @param suggestionId - The suggestion ID for this operation
104
+ * @returns The new editor state after applying the delete
105
+ */
106
+ export declare const applyDeleteAtTag: (doc: TaggedNode, state: EditorState, tagName: string, suggestionId?: number) => EditorState;
@@ -0,0 +1,3 @@
1
+ import type { Transaction } from "prosemirror-state";
2
+ import type { MarkType } from "prosemirror-model";
3
+ export declare const joinBlocks: (trackedTransaction: Transaction, stepFrom: number, stepTo: number, insertionMarkType: MarkType) => boolean;