@magic-marker/prosemirror-suggest-changes 0.3.3-wrap-unwrap.13 → 0.3.3-wrap-unwrap.14

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.
@@ -140,9 +140,9 @@ export declare function assertReverted(finalState: EditorState, initialState: Ed
140
140
  * This helper replaces the editor document with the provided JSON,
141
141
  * clears transactions, and returns initial state.
142
142
  */
143
- export declare function setupDocFromJSON(page: Page, docJSON: unknown): Promise<{
143
+ export declare function setupDocFromJSON(page: Page, docJSON: object): Promise<{
144
144
  initialState: EditorState;
145
- initialDoc: unknown;
145
+ initialDoc: object;
146
146
  }>;
147
147
  /**
148
148
  * Assert that document fully reverted to initial state.
@@ -2,6 +2,7 @@ import { Mark } from "prosemirror-model";
2
2
  import { canJoin, Transform } from "prosemirror-transform";
3
3
  import { ZWSP } from "../../constants.js";
4
4
  import { getSuggestionMarks } from "../../utils.js";
5
+ import { guardStructureMarkAttrs } from "../wrapUnwrap/types.js";
5
6
  export function isJoinMarkAttrs(attrs) {
6
7
  if (attrs["type"] !== "join") return false;
7
8
  if (attrs["data"] == null) return false;
@@ -94,7 +95,9 @@ export function maybeRevertJoinMark(tr, from, to, node, markType) {
94
95
  attrs: $endOfNode.nodeAfter.attrs,
95
96
  marks: $endOfNode.nodeAfter.marks.map((mark)=>mark.toJSON())
96
97
  };
98
+ const shouldSuppressJoinMark = hasStructureAddMark($endOfNode.nodeBefore) || hasStructureAddMark($endOfNode.nodeAfter);
97
99
  transform.join(mappedEndOfNode);
100
+ if (shouldSuppressJoinMark) return false;
98
101
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99
102
  const joinStep = transform.steps[transform.steps.length - 1];
100
103
  const joinPos = joinStep.getMap().map(mappedEndOfNode);
@@ -111,6 +114,14 @@ export function maybeRevertJoinMark(tr, from, to, node, markType) {
111
114
  });
112
115
  return transform;
113
116
  }
117
+ function hasStructureAddMark(node) {
118
+ const { structure } = getSuggestionMarks(node.type.schema);
119
+ return node.marks.some((mark)=>{
120
+ if (mark.type !== structure) return false;
121
+ if (!guardStructureMarkAttrs(mark.attrs)) return false;
122
+ return mark.attrs.data.op.op === "add";
123
+ });
124
+ }
114
125
  /**
115
126
  * Find ZWSP nodes marked as insertions and deletions with the same mark id
116
127
  * Delete them from the given range
@@ -30,6 +30,7 @@ export function buildMaterializedPaths(doc) {
30
30
  };
31
31
  paths.set(nodeId, {
32
32
  nodeType: node.type.name,
33
+ node,
33
34
  chain: [
34
35
  parent
35
36
  ]
@@ -71,6 +72,7 @@ export function buildMaterializedPaths(doc) {
71
72
  ];
72
73
  paths.set(nodeId, {
73
74
  nodeType: node.type.name,
75
+ node,
74
76
  chain
75
77
  });
76
78
  return true;
@@ -4,11 +4,14 @@ import { type SuggestionId } from "../../generateId.js";
4
4
  import { type StructuralContextPath } from "./types.js";
5
5
  import { Transform } from "prosemirror-transform";
6
6
  export declare const structureChangesKey: PluginKey<any>;
7
+ export type SuggestStructureChangesReason = "split-derived-add";
8
+ export interface SuggestStructureChangesResult {
9
+ handled: boolean;
10
+ transform: Transform;
11
+ reason?: SuggestStructureChangesReason;
12
+ }
7
13
  export declare function structureChangesPlugin(generateId?: (schema: Schema, doc?: Node) => SuggestionId, opts?: {
8
14
  experimental_trackStructures?: StructuralContextPath[];
9
15
  }): Plugin<any>;
10
- export declare function suggestStructureChanges(docBefore: Node, docAfter: Node, structuralContextPaths: StructuralContextPath[], generateId?: (schema: Schema, doc?: Node) => SuggestionId): {
11
- handled: boolean;
12
- transform: Transform;
13
- };
16
+ export declare function suggestStructureChanges(docBefore: Node, docAfter: Node, structuralContextPaths: StructuralContextPath[], generateId?: (schema: Schema, doc?: Node) => SuggestionId): SuggestStructureChangesResult;
14
17
  export declare function getRequiredStructuralContextPaths(structuralContextPaths: StructuralContextPath[] | undefined): StructuralContextPath[];
@@ -112,11 +112,19 @@ export function suggestStructureChanges(docBefore, docAfter, structuralContextPa
112
112
  pathsBefore: Object.fromEntries(pathsBefore.entries()),
113
113
  pathsAfter: Object.fromEntries(pathsAfter.entries())
114
114
  });
115
- const ops = getOps(pathsBefore, pathsAfter, structuralContextPaths);
115
+ const { ops, reason } = getOps(pathsBefore, pathsAfter, structuralContextPaths);
116
116
  trace("suggestStructureChanges", "ops", {
117
- ops: Object.fromEntries(ops.entries())
117
+ ops: Object.fromEntries(ops.entries()),
118
+ reason
118
119
  });
119
120
  const transform = new Transform(docAfter);
121
+ if (reason) {
122
+ return {
123
+ handled: false,
124
+ transform,
125
+ reason
126
+ };
127
+ }
120
128
  addMarks(ops, transform, suggestionId);
121
129
  return {
122
130
  handled: ops.size > 0,
@@ -206,13 +214,22 @@ function getOps(beforePaths, afterPaths, structuralContextPaths) {
206
214
  const hasStructuralContext = chainHasStructuralContext(afterPath.chain, structuralContextPaths);
207
215
  // node is outside configured structural contexts
208
216
  if (!hasStructuralContext) continue;
217
+ // detect block split - if detected, bail out, and let the main plugin handle it
218
+ if (isSplitDerivedAdd(id, beforePaths, afterPaths, contextNodeTypes)) {
219
+ return {
220
+ ops: new Map(),
221
+ reason: "split-derived-add"
222
+ };
223
+ }
209
224
  // node was added
210
225
  const op = {
211
226
  op: "add"
212
227
  };
213
228
  ops.set(id, op);
214
229
  }
215
- return ops;
230
+ return {
231
+ ops
232
+ };
216
233
  }
217
234
  function getStructuralContextNodeTypes(structuralContextPaths) {
218
235
  return new Set(structuralContextPaths.flat());
@@ -233,3 +250,45 @@ function containsContiguousPath(chainNodeTypes, structuralContextPath) {
233
250
  }
234
251
  return false;
235
252
  }
253
+ // detect block split - try to look at the node above, concatenate two text contents,
254
+ // and see if it combines into a single node in the old document
255
+ function isSplitDerivedAdd(newNodeId, beforePaths, afterPaths, contextNodeTypes) {
256
+ const newNode = afterPaths.get(newNodeId)?.node;
257
+ if (!newNode || newNode.textContent === "") return false;
258
+ if (matchesSplitDerivedPair(newNodeId, newNode.textContent, beforePaths, afterPaths)) {
259
+ return true;
260
+ }
261
+ const afterPath = afterPaths.get(newNodeId);
262
+ if (!afterPath) return false;
263
+ for (const parent of afterPath.chain){
264
+ if (parent.nodeType === DOC_NODE_ID) continue;
265
+ if (!contextNodeTypes.has(parent.nodeType)) continue;
266
+ const parentNode = afterPaths.get(parent.nodeId)?.node;
267
+ if (!parentNode || parentNode.textContent === "") continue;
268
+ if (matchesSplitDerivedPair(parent.nodeId, parentNode.textContent, beforePaths, afterPaths)) {
269
+ return true;
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+ function matchesSplitDerivedPair(rightNodeId, rightText, beforePaths, afterPaths) {
275
+ const rightPath = afterPaths.get(rightNodeId);
276
+ const previousSiblingId = rightPath?.chain[0]?.childSiblingIds[0];
277
+ if (!previousSiblingId) return false;
278
+ const previousBefore = beforePaths.has(previousSiblingId) ? beforePaths.get(previousSiblingId)?.node : null;
279
+ const previousAfter = afterPaths.has(previousSiblingId) ? afterPaths.get(previousSiblingId)?.node : null;
280
+ if (!previousBefore || !previousAfter) return false;
281
+ if (hasStructureAddMarkInSubtree(previousBefore)) return false;
282
+ return previousBefore.textContent === previousAfter.textContent + rightText;
283
+ }
284
+ function hasStructureAddMarkInSubtree(node) {
285
+ if (hasStructureAddMark(node)) return true;
286
+ let found = false;
287
+ node.descendants((descendant)=>{
288
+ if (descendant.isText) return false;
289
+ if (!hasStructureAddMark(descendant)) return true;
290
+ found = true;
291
+ return false;
292
+ });
293
+ return found;
294
+ }
@@ -1,4 +1,4 @@
1
- import { type Attrs } from "prosemirror-model";
1
+ import { type Attrs, type Node } from "prosemirror-model";
2
2
  import { type SuggestionId } from "../../generateId.js";
3
3
  import { DOC_NODE_ID } from "./constants.js";
4
4
  interface NodeParent {
@@ -33,6 +33,7 @@ export type Op = MoveOp | AddOp;
33
33
  export type MaterializedPaths = Map<string, {
34
34
  chain: Parent[];
35
35
  nodeType: string;
36
+ node: Node;
36
37
  }>;
37
38
  export declare function guardDocParent(parent: Parent | undefined): parent is DocParent;
38
39
  export declare function guardStructureMarkAttrs(attrs: Attrs): attrs is StructureMarkAttrs;
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.13",
3
+ "version": "0.3.3-wrap-unwrap.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",