@magic-marker/prosemirror-suggest-changes 0.3.3-wrap-unwrap.31 → 0.3.3-wrap-unwrap.32

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.
@@ -9,6 +9,8 @@ export declare class EditorPage {
9
9
  getParagraphText(index: number, childIndexes?: number[]): Promise<string>;
10
10
  getParagraphAt(index: number): Locator;
11
11
  getParagraphCount(): Promise<number>;
12
+ getListItems(): Locator;
13
+ getParagraphs(): Locator;
12
14
  getListItemCount(): Promise<number>;
13
15
  getProseMirrorMarkCount(name: string): Promise<number>;
14
16
  getProseMirrorMarksJSON(): Promise<unknown[]>;
package/dist/commands.js CHANGED
@@ -5,6 +5,7 @@ import { suggestChangesKey } from "./plugin.js";
5
5
  import { getSuggestionMarks } from "./utils.js";
6
6
  import { ZWSP } from "./constants.js";
7
7
  import { maybeRevertJoinMark } from "./features/joinOnDelete/index.js";
8
+ import { isJoinMark } from "./features/joinOnDelete/types.js";
8
9
  import { revertAllStructureSuggestions, revertStructureSuggestion } from "./features/wrapUnwrap/revert/index.js";
9
10
  import { applyAllStructureSuggestions, applyStructureSuggestion } from "./features/wrapUnwrap/apply/index.js";
10
11
  /**
@@ -65,7 +66,9 @@ import { applyAllStructureSuggestions, applyStructureSuggestion } from "./featur
65
66
  tr.removeMark(insertionFrom, insertionTo, markTypeToApply);
66
67
  const joinRevertResult = maybeRevertJoinMark(tr, insertionFrom, insertionTo, child, markTypeToApply);
67
68
  // reverting a join mark may produce new structure marks that were serialized in the join metadata
68
- if (joinRevertResult) joinRevertResult.restoredStructureSuggestionIds.forEach((id)=>restoredStructureSuggestionIds.add(id));
69
+ if (joinRevertResult) {
70
+ joinRevertResult.restoredStructureSuggestionIds.forEach((id)=>restoredStructureSuggestionIds.add(id));
71
+ }
69
72
  if (!joinRevertResult && child.text === ZWSP) {
70
73
  tr.delete(insertionFrom, insertionTo);
71
74
  }
@@ -78,6 +81,47 @@ import { applyAllStructureSuggestions, applyStructureSuggestion } from "./featur
78
81
  restoredStructureSuggestionIds
79
82
  };
80
83
  }
84
+ /**
85
+ * Collect suggestion IDs of join marks in the node
86
+ *
87
+ * @param node
88
+ * @param deletion
89
+ * @returns an array of suggestion IDs
90
+ */ function findJoinSuggestionIds(node, deletion) {
91
+ const joinSuggestionIds = [];
92
+ node.descendants((child)=>{
93
+ const mark = deletion.isInSet(child.marks);
94
+ if (!mark || !isJoinMark(mark)) return true;
95
+ if (!child.isText || child.text !== ZWSP) return true;
96
+ joinSuggestionIds.push(mark.attrs.id);
97
+ return true;
98
+ });
99
+ return joinSuggestionIds.reverse();
100
+ }
101
+ /**
102
+ * Revert suggestions revealed by reverting a join mark
103
+ * Prioritize reverting revealed suggestions with the same id as the join mark
104
+ * (that means they are linked to the join mark and has to be reverted together as one)
105
+ * Revert other revealed suggestions as well so the user doesn't have to revert multiple times at the same place
106
+ *
107
+ * @param tr
108
+ * @param suggestionId
109
+ * @param restoredStructureSuggestionIds
110
+ */ function revertRestoredStructureSuggestions(tr, suggestionId, restoredStructureSuggestionIds) {
111
+ if (restoredStructureSuggestionIds.has(suggestionId)) {
112
+ const restoredStructureTransform = revertStructureSuggestion(tr.doc, suggestionId);
113
+ restoredStructureTransform.steps.forEach((step)=>{
114
+ tr.step(step);
115
+ });
116
+ }
117
+ restoredStructureSuggestionIds.forEach((restoredSuggestionId)=>{
118
+ if (restoredSuggestionId === suggestionId) return;
119
+ const restoredStructureTransform = revertStructureSuggestion(tr.doc, restoredSuggestionId);
120
+ restoredStructureTransform.steps.forEach((step)=>{
121
+ tr.step(step);
122
+ });
123
+ });
124
+ }
81
125
  function revertModifications(node, pos, tr) {
82
126
  const { modification } = getSuggestionMarks(node.type.schema);
83
127
  const existingMods = node.marks.filter((mark)=>mark.type === modification);
@@ -291,7 +335,17 @@ export function applySuggestionsToRange(doc, from, to) {
291
335
  const { deletion, insertion } = getSuggestionMarks(state.schema);
292
336
  // create a structure transform that reverts all structure changes on the given document
293
337
  const structureTransform = revertAllStructureSuggestions(state.doc);
294
- // then start a clear transform from the document where the structure changes are reverted
338
+ // revert all join marks first as well as any structure marks they contain serialized
339
+ const joinSuggestionIds = findJoinSuggestionIds(structureTransform.doc, deletion);
340
+ joinSuggestionIds.forEach((suggestionId)=>{
341
+ const suggestionsTransform = new Transform(structureTransform.doc);
342
+ const { restoredStructureSuggestionIds } = applySuggestionsToTransform(suggestionsTransform.doc, suggestionsTransform, deletion, insertion, suggestionId);
343
+ suggestionsTransform.steps.forEach((step)=>{
344
+ structureTransform.step(step);
345
+ });
346
+ revertRestoredStructureSuggestions(structureTransform, suggestionId, restoredStructureSuggestionIds);
347
+ });
348
+ // then start a clear transform from the document where the structure changes and join marks are reverted
295
349
  const suggestionsTransform = new Transform(structureTransform.doc);
296
350
  applySuggestionsToTransform(suggestionsTransform.doc, suggestionsTransform, deletion, insertion);
297
351
  applyModificationsToTransform(suggestionsTransform.doc, suggestionsTransform, -1);
@@ -370,12 +424,9 @@ export function applySuggestionsToRange(doc, from, to) {
370
424
  suggestionsTransform.steps.forEach((step)=>{
371
425
  structureTransform.step(step);
372
426
  });
373
- restoredStructureSuggestionIds.forEach((suggestionId)=>{
374
- const restoredStructureTransform = revertStructureSuggestion(structureTransform.doc, suggestionId);
375
- restoredStructureTransform.steps.forEach((step)=>{
376
- structureTransform.step(step);
377
- });
378
- });
427
+ // in case there are structure marks revealed after join mark revert,
428
+ // revert them as well
429
+ revertRestoredStructureSuggestions(structureTransform, suggestionId, restoredStructureSuggestionIds);
379
430
  // apply the structure transform to the transaction
380
431
  const transaction = state.tr;
381
432
  structureTransform.steps.forEach((step)=>{
@@ -1,4 +1,5 @@
1
1
  import { type Mark, type Attrs, type Node } from "prosemirror-model";
2
+ import { type SuggestionId } from "../../generateId.js";
2
3
  export interface SerializedJoinNode {
3
4
  type: string;
4
5
  attrs: object;
@@ -7,6 +8,7 @@ export interface SerializedJoinNode {
7
8
  }[];
8
9
  }
9
10
  export interface JoinMarkAttrs {
11
+ id: SuggestionId;
10
12
  type: "join";
11
13
  data: {
12
14
  leftNode?: SerializedJoinNode;
@@ -5,6 +5,9 @@ export function isSerializedJoinNode(node) {
5
5
  return typeof data.type === "string" && typeof data.attrs === "object" && data.attrs !== null && Array.isArray(data.marks);
6
6
  }
7
7
  export function isJoinMarkAttrs(attrs) {
8
+ if (typeof attrs["id"] !== "string" && typeof attrs["id"] !== "number") {
9
+ return false;
10
+ }
8
11
  if (attrs["type"] !== "join") return false;
9
12
  if (attrs["data"] == null) return false;
10
13
  return true;
@@ -0,0 +1,6 @@
1
+ import { type Selection } from "prosemirror-state";
2
+ import { type ReplaceStep, type Step } from "prosemirror-transform";
3
+ export declare function adjustForStartToStartTextblockDeletion(selection: Selection, step: ReplaceStep, prevSteps: Step[]): {
4
+ from: number;
5
+ to: number;
6
+ };
@@ -0,0 +1,54 @@
1
+ import { TextSelection } from "prosemirror-state";
2
+ export function adjustForStartToStartTextblockDeletion(selection, step, prevSteps) {
3
+ if (prevSteps.length > 0) return {
4
+ from: step.from,
5
+ to: step.to
6
+ };
7
+ if (!(selection instanceof TextSelection)) {
8
+ return {
9
+ from: step.from,
10
+ to: step.to
11
+ };
12
+ }
13
+ if (selection.empty || step.slice.size !== 0) {
14
+ return {
15
+ from: step.from,
16
+ to: step.to
17
+ };
18
+ }
19
+ const { $from, $to } = selection;
20
+ if (!$from.parent.isTextblock || !$to.parent.isTextblock) {
21
+ return {
22
+ from: step.from,
23
+ to: step.to
24
+ };
25
+ }
26
+ if ($from.parentOffset !== 0 || $to.parentOffset !== 0) {
27
+ return {
28
+ from: step.from,
29
+ to: step.to
30
+ };
31
+ }
32
+ if ($from.start() === $to.start()) return {
33
+ from: step.from,
34
+ to: step.to
35
+ };
36
+ if (step.from !== $from.before($from.depth)) {
37
+ return {
38
+ from: step.from,
39
+ to: step.to
40
+ };
41
+ }
42
+ if (step.to !== $to.before($to.depth)) {
43
+ return {
44
+ from: step.from,
45
+ to: step.to
46
+ };
47
+ }
48
+ // The step boundary is moved before the right textblock, but the selection
49
+ // boundary is the user-visible end of the deleted text range.
50
+ return {
51
+ from: step.from,
52
+ to: selection.to
53
+ };
54
+ }
@@ -1,5 +1,6 @@
1
1
  import { EditorState, Selection } from "prosemirror-state";
2
2
  import { preserveTransactionData, transformToSuggestionTransaction } from "../../../transformToSuggestionTransaction.js";
3
+ import { generateNextNumberId } from "../../../generateId.js";
3
4
  import { suggestStructureChanges } from "../../wrapUnwrap/structureChangesPlugin.js";
4
5
  import { detectSpecialTransactionShape } from "../detectSpecialTransactionShape.js";
5
6
  // handle the specific TipTap pattern when backspacing from a paragraph into a list above
@@ -14,6 +15,8 @@ export function handleTipTapParagraphIntoListJoin(args) {
14
15
  const docBefore = args.transaction.docs[0];
15
16
  if (!docBefore) return null;
16
17
  const trackedTransaction = args.state.tr;
18
+ const sharedSuggestionId = args.generateId ? args.generateId(args.state.schema, docBefore) : generateNextNumberId(args.state.schema, docBefore);
19
+ const generateSharedSuggestionId = ()=>sharedSuggestionId;
17
20
  try {
18
21
  trackedTransaction.step(shape.deleteStep);
19
22
  trackedTransaction.step(shape.insertStep);
@@ -26,7 +29,7 @@ export function handleTipTapParagraphIntoListJoin(args) {
26
29
  uniqueNodeIdsTransform.steps.forEach((step)=>{
27
30
  trackedTransaction.step(step);
28
31
  });
29
- const structureChangesResult = suggestStructureChanges(docBefore, uniqueNodeIdsTransform.doc, args.structuralContextPaths, args.generateId);
32
+ const structureChangesResult = suggestStructureChanges(docBefore, uniqueNodeIdsTransform.doc, args.structuralContextPaths, generateSharedSuggestionId);
30
33
  if (!structureChangesResult.handled) {
31
34
  return null;
32
35
  }
@@ -43,7 +46,7 @@ export function handleTipTapParagraphIntoListJoin(args) {
43
46
  } catch {
44
47
  return null;
45
48
  }
46
- const trackedJoinTransaction = transformToSuggestionTransaction(joinTransaction, intermediateState, args.generateId);
49
+ const trackedJoinTransaction = transformToSuggestionTransaction(joinTransaction, intermediateState, generateSharedSuggestionId);
47
50
  trackedJoinTransaction.steps.forEach((step)=>{
48
51
  trackedTransaction.step(step);
49
52
  });
@@ -4,6 +4,8 @@ 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 { adjustForStartToStartTextblockDeletion } from "./features/startToStartTextblockDeletion/index.js";
8
+ // import { rebaseStep } from "./rebaseStep.js";
7
9
  /**
8
10
  * Transform a replace step into its equivalent tracked steps.
9
11
  *
@@ -35,17 +37,18 @@ import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSP
35
37
  * zero-width spaces will be removed.
36
38
  */ export function suggestReplaceStep(trackedTransaction, state, doc, step, prevSteps, suggestionId) {
37
39
  const { deletion, insertion } = getSuggestionMarks(state.schema);
40
+ const semanticStep = adjustForStartToStartTextblockDeletion(state.selection, step, prevSteps);
38
41
  // Check for insertion and deletion marks directly
39
42
  // adjacent to this step's boundaries. If they exist,
40
43
  // we'll use their ids, rather than producing a new one
41
- const nodeBefore = doc.resolve(step.from).nodeBefore;
44
+ const nodeBefore = doc.resolve(semanticStep.from).nodeBefore;
42
45
  const markBefore = nodeBefore?.marks.find((mark)=>mark.type === deletion || mark.type === insertion) ?? null;
43
- const nodeAfter = doc.resolve(step.to).nodeAfter;
46
+ const nodeAfter = doc.resolve(semanticStep.to).nodeAfter;
44
47
  const markAfter = nodeAfter?.marks.find((mark)=>mark.type === deletion || mark.type === insertion) ?? null;
45
48
  let markId = markBefore?.attrs["id"] ?? markAfter?.attrs["id"] ?? suggestionId;
46
49
  // Rebase this step's boundaries onto the newest doc
47
- let stepFrom = rebasePos(step.from, prevSteps, trackedTransaction.steps);
48
- let stepTo = rebasePos(step.to, prevSteps, trackedTransaction.steps);
50
+ let stepFrom = rebasePos(semanticStep.from, prevSteps, trackedTransaction.steps);
51
+ let stepTo = rebasePos(semanticStep.to, prevSteps, trackedTransaction.steps);
49
52
  if (state.selection.empty && stepFrom !== stepTo) {
50
53
  trackedTransaction.setSelection(TextSelection.near(trackedTransaction.doc.resolve(stepFrom)));
51
54
  }
@@ -58,8 +61,8 @@ import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSP
58
61
  }
59
62
  // Update the step boundaries, since we may have just changed
60
63
  // the document
61
- stepFrom = rebasePos(step.from, prevSteps, trackedTransaction.steps);
62
- stepTo = rebasePos(step.to, prevSteps, trackedTransaction.steps);
64
+ stepFrom = rebasePos(semanticStep.from, prevSteps, trackedTransaction.steps);
65
+ stepTo = rebasePos(semanticStep.to, prevSteps, trackedTransaction.steps);
63
66
  // Re-resolve nodeAfter and markAfter if we did a block join
64
67
  // The original values are stale after joinBlocks modifies the document
65
68
  let nodeAfterResolved = nodeAfter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-marker/prosemirror-suggest-changes",
3
- "version": "0.3.3-wrap-unwrap.31",
3
+ "version": "0.3.3-wrap-unwrap.32",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",
@@ -55,17 +55,17 @@
55
55
  "markdown-toc": "^1.2.0",
56
56
  "playwright": "^1.56.0",
57
57
  "prettier": "^3.5.3",
58
- "prosemirror-commands": "^1.7.0",
59
- "prosemirror-history": "^1.5.0",
60
- "prosemirror-inputrules": "^1.4.0",
61
- "prosemirror-keymap": "^1.2.2",
62
- "prosemirror-model": "^1.24.1",
63
- "prosemirror-schema-basic": "^1.2.3",
64
- "prosemirror-schema-list": "^1.5.0",
65
- "prosemirror-state": "^1.4.3",
66
- "prosemirror-test-builder": "^1.1.1",
67
- "prosemirror-transform": "^1.10.2",
68
- "prosemirror-view": "^1.38.0",
58
+ "prosemirror-commands": "1.7.1",
59
+ "prosemirror-history": "1.5.0",
60
+ "prosemirror-inputrules": "1.5.1",
61
+ "prosemirror-keymap": "1.2.3",
62
+ "prosemirror-model": "1.25.4",
63
+ "prosemirror-schema-basic": "1.2.4",
64
+ "prosemirror-schema-list": "1.5.1",
65
+ "prosemirror-state": "1.4.4",
66
+ "prosemirror-test-builder": "1.1.1",
67
+ "prosemirror-transform": "1.12.0",
68
+ "prosemirror-view": "1.41.9",
69
69
  "remark-parse": "^11.0.0",
70
70
  "rimraf": "^6.0.1",
71
71
  "typescript": "^5.8.2",
@@ -79,5 +79,17 @@
79
79
  "prosemirror-state": "^1.0.0",
80
80
  "prosemirror-transform": "^1.0.0",
81
81
  "prosemirror-view": "^1.0.0"
82
+ },
83
+ "resolutions": {
84
+ "prosemirror-model": "1.25.4",
85
+ "prosemirror-state": "1.4.4",
86
+ "prosemirror-transform": "1.12.0",
87
+ "prosemirror-view": "1.41.9",
88
+ "prosemirror-keymap": "1.2.3",
89
+ "prosemirror-commands": "1.7.1",
90
+ "prosemirror-schema-basic": "1.2.4",
91
+ "prosemirror-schema-list": "1.5.1",
92
+ "prosemirror-history": "1.5.0",
93
+ "prosemirror-inputrules": "1.5.1"
82
94
  }
83
95
  }