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

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.
@@ -1,4 +1,5 @@
1
1
  import { getNodeId } from "./getNodeId.js";
2
+ import { DOC_NODE_ID } from "./constants.js";
2
3
  const TRACE_ENABLED = true;
3
4
  function trace(...args) {
4
5
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -17,8 +18,8 @@ export function buildMaterializedPaths(doc) {
17
18
  const rightSibling = children[index + 1];
18
19
  const rightSiblingId = rightSibling ? getNodeId(rightSibling) : null;
19
20
  const parent = {
20
- nodeId: "__doc__",
21
- nodeType: "__doc__",
21
+ nodeId: DOC_NODE_ID,
22
+ nodeType: DOC_NODE_ID,
22
23
  nodeAttrs: {},
23
24
  nodeMarks: [],
24
25
  childSiblingIds: [
@@ -1,3 +1,3 @@
1
+ export declare const DOC_NODE_ID = "__doc__";
1
2
  export declare const STRUCTURE_CHANGES_ADD_MARKS = "structure-changes-add-mark-type";
2
3
  export declare const STRUCTURE_CHANGES_REVERT_MARKS = "structure-changes-revert-mark-type";
3
- export declare const LIST_NODES: string[];
@@ -1,7 +1,3 @@
1
+ export const DOC_NODE_ID = "__doc__";
1
2
  export const STRUCTURE_CHANGES_ADD_MARKS = "structure-changes-add-mark-type";
2
3
  export const STRUCTURE_CHANGES_REVERT_MARKS = "structure-changes-revert-mark-type";
3
- export const LIST_NODES = [
4
- "orderedList",
5
- "bulletList",
6
- "listItem"
7
- ];
@@ -118,5 +118,5 @@ export function findInsertionPos(node, pos, parent, child) {
118
118
  return leftSibling.pos + leftSibling.node.nodeSize;
119
119
  }
120
120
  // insert at end of node
121
- return pos != null ? pos + node.nodeSize - 1 : node.nodeSize - 1;
121
+ return pos != null ? pos + node.nodeSize - 1 : node.content.size;
122
122
  }
@@ -1,10 +1,14 @@
1
1
  import { type Schema, type Node } from "prosemirror-model";
2
2
  import { Plugin, PluginKey } from "prosemirror-state";
3
3
  import { type SuggestionId } from "../../generateId.js";
4
+ import { type StructuralContextPath } from "./types.js";
4
5
  import { Transform } from "prosemirror-transform";
5
6
  export declare const structureChangesKey: PluginKey<any>;
6
- export declare function structureChangesPlugin(generateId?: (schema: Schema, doc?: Node) => SuggestionId): Plugin<any>;
7
- export declare function suggestStructureChanges(docBefore: Node, docAfter: Node, generateId?: (schema: Schema, doc?: Node) => SuggestionId): {
7
+ export declare function structureChangesPlugin(generateId?: (schema: Schema, doc?: Node) => SuggestionId, opts?: {
8
+ experimental_trackStructures?: StructuralContextPath[];
9
+ }): Plugin<any>;
10
+ export declare function suggestStructureChanges(docBefore: Node, docAfter: Node, structuralContextPaths: StructuralContextPath[], generateId?: (schema: Schema, doc?: Node) => SuggestionId): {
8
11
  handled: boolean;
9
12
  transform: Transform;
10
13
  };
14
+ export declare function getRequiredStructuralContextPaths(structuralContextPaths: StructuralContextPath[] | undefined): StructuralContextPath[];
@@ -3,7 +3,7 @@ import { getSuggestionMarks } from "../../utils.js";
3
3
  import { generateNextNumberId } from "../../generateId.js";
4
4
  import { getNodeId } from "./getNodeId.js";
5
5
  import { guardStructureMarkAttrs } from "./types.js";
6
- import { LIST_NODES, STRUCTURE_CHANGES_ADD_MARKS } from "./constants.js";
6
+ import { DOC_NODE_ID, STRUCTURE_CHANGES_ADD_MARKS } from "./constants.js";
7
7
  import { Transform } from "prosemirror-transform";
8
8
  import { isSuggestChangesEnabled, suggestChangesKey } from "../../plugin.js";
9
9
  import { buildMaterializedPaths } from "./buildMaterializedPaths.js";
@@ -15,12 +15,8 @@ function trace(...args) {
15
15
  console.log("[structureChanges]", ...args);
16
16
  }
17
17
  export const structureChangesKey = new PluginKey("@handlewithcare/prosemirror-suggest-changes-structure-changes");
18
- // const listStructure = {
19
- // type: [LIST_NODE],
20
- // children: [{ type: [LIST_ITEM_NODE] }],
21
- // };
22
- // const structures = [listStructure];
23
- export function structureChangesPlugin(generateId) {
18
+ export function structureChangesPlugin(generateId, opts) {
19
+ const structuralContextPaths = getRequiredStructuralContextPaths(opts?.experimental_trackStructures);
24
20
  return new Plugin({
25
21
  key: structureChangesKey,
26
22
  appendTransaction (transactions, _oldState, newState) {
@@ -92,7 +88,7 @@ export function structureChangesPlugin(generateId) {
92
88
  trace("structureChangesPlugin", "appendTransaction", [
93
89
  ...transactions
94
90
  ]);
95
- const { transform } = suggestStructureChanges(oldDoc, newDoc, generateId);
91
+ const { transform } = suggestStructureChanges(oldDoc, newDoc, structuralContextPaths, generateId);
96
92
  const tr = newState.tr;
97
93
  transform.steps.forEach((step)=>{
98
94
  tr.step(step);
@@ -108,7 +104,7 @@ function isEnabled(tr, editorState) {
108
104
  const isEnabled = isSuggestChangesEnabled(editorState) && !tr.getMeta("history$") && !tr.getMeta("collab$") && !ySyncMeta.isUndoRedoOperation && !ySyncMeta.isChangeOrigin && !("skip" in (tr.getMeta(suggestChangesKey) ?? {}));
109
105
  return isEnabled;
110
106
  }
111
- export function suggestStructureChanges(docBefore, docAfter, generateId) {
107
+ export function suggestStructureChanges(docBefore, docAfter, structuralContextPaths, generateId) {
112
108
  const suggestionId = generateId ? generateId(docBefore.type.schema, docBefore) : generateNextNumberId(docBefore.type.schema, docBefore);
113
109
  const pathsBefore = buildMaterializedPaths(docBefore);
114
110
  const pathsAfter = buildMaterializedPaths(docAfter);
@@ -116,7 +112,7 @@ export function suggestStructureChanges(docBefore, docAfter, generateId) {
116
112
  pathsBefore: Object.fromEntries(pathsBefore.entries()),
117
113
  pathsAfter: Object.fromEntries(pathsAfter.entries())
118
114
  });
119
- const ops = getOps(pathsBefore, pathsAfter);
115
+ const ops = getOps(pathsBefore, pathsAfter, structuralContextPaths);
120
116
  trace("suggestStructureChanges", "ops", {
121
117
  ops: Object.fromEntries(ops.entries())
122
118
  });
@@ -127,6 +123,12 @@ export function suggestStructureChanges(docBefore, docAfter, generateId) {
127
123
  transform
128
124
  };
129
125
  }
126
+ export function getRequiredStructuralContextPaths(structuralContextPaths) {
127
+ if (!structuralContextPaths?.length) {
128
+ throw new Error("experimental_trackStructures must be provided when structure tracking is enabled");
129
+ }
130
+ return structuralContextPaths;
131
+ }
130
132
  function addMarks(ops, tr, suggestionId) {
131
133
  const perfAddMarks = performance.now();
132
134
  const { structure } = getSuggestionMarks(tr.doc.type.schema);
@@ -172,20 +174,22 @@ function hasStructureAddMark(node) {
172
174
  return mark.attrs.data.op.op === "add";
173
175
  });
174
176
  }
175
- function getOps(beforePaths, afterPaths) {
177
+ function getOps(beforePaths, afterPaths, structuralContextPaths) {
176
178
  const ops = new Map();
179
+ const contextNodeTypes = getStructuralContextNodeTypes(structuralContextPaths);
177
180
  // first take care of nodes that exist in both
178
181
  for (const [id, beforePath] of beforePaths){
179
- if (LIST_NODES.includes(beforePath.nodeType)) continue;
182
+ if (contextNodeTypes.has(beforePath.nodeType)) continue;
180
183
  const afterPath = afterPaths.get(id);
181
184
  // node was removed - do nothing
182
185
  if (afterPath == null) continue;
186
+ if (contextNodeTypes.has(afterPath.nodeType)) continue;
183
187
  const sameChain = sameParentChain(beforePath.chain, afterPath.chain);
184
188
  // node did not move anywhere - do nothing
185
189
  if (sameChain) continue;
186
- const hasList = beforePath.chain.some((parent)=>LIST_NODES.includes(parent.nodeType)) || afterPath.chain.some((parent)=>LIST_NODES.includes(parent.nodeType));
187
- // node is outside lists
188
- if (!hasList) continue;
190
+ const hasStructuralContext = chainHasStructuralContext(beforePath.chain, structuralContextPaths) || chainHasStructuralContext(afterPath.chain, structuralContextPaths);
191
+ // node is outside configured structural contexts
192
+ if (!hasStructuralContext) continue;
189
193
  const op = {
190
194
  op: "move",
191
195
  from: beforePath.chain,
@@ -196,12 +200,12 @@ function getOps(beforePaths, afterPaths) {
196
200
  // now take care of nodes that exist only in afterPaths
197
201
  // (we don't care about nodes that exist only in beforePaths - they were deleted)
198
202
  for (const [id, afterPath] of afterPaths){
199
- if (LIST_NODES.includes(afterPath.nodeType)) continue;
203
+ if (contextNodeTypes.has(afterPath.nodeType)) continue;
200
204
  // ignore nodes that also exist in beforePaths - they are already handled
201
205
  if (beforePaths.has(id)) continue;
202
- const hasList = afterPath.chain.some((parent)=>LIST_NODES.includes(parent.nodeType));
203
- // node is outside lists
204
- if (!hasList) continue;
206
+ const hasStructuralContext = chainHasStructuralContext(afterPath.chain, structuralContextPaths);
207
+ // node is outside configured structural contexts
208
+ if (!hasStructuralContext) continue;
205
209
  // node was added
206
210
  const op = {
207
211
  op: "add"
@@ -210,3 +214,22 @@ function getOps(beforePaths, afterPaths) {
210
214
  }
211
215
  return ops;
212
216
  }
217
+ function getStructuralContextNodeTypes(structuralContextPaths) {
218
+ return new Set(structuralContextPaths.flat());
219
+ }
220
+ function chainHasStructuralContext(chain, structuralContextPaths) {
221
+ const topDownChain = [
222
+ ...chain
223
+ ].reverse().filter((parent)=>parent.nodeType !== DOC_NODE_ID).map((parent)=>parent.nodeType);
224
+ return structuralContextPaths.some((path)=>containsContiguousPath(topDownChain, path));
225
+ }
226
+ // does a chain like: __doc__->nodeTypeA->nodeTypeB->nodeTypeC->nodeTypeD
227
+ // contain a structural context path (a "sub chain") like nodeTypeB->nodeTypeC ?
228
+ function containsContiguousPath(chainNodeTypes, structuralContextPath) {
229
+ if (structuralContextPath.length > chainNodeTypes.length) return false;
230
+ for(let start = 0; start <= chainNodeTypes.length - structuralContextPath.length; start++){
231
+ const matches = structuralContextPath.every((nodeType, index)=>chainNodeTypes[start + index] === nodeType);
232
+ if (matches) return true;
233
+ }
234
+ return false;
235
+ }
@@ -1,5 +1,6 @@
1
1
  import { type Attrs } from "prosemirror-model";
2
2
  import { type SuggestionId } from "../../generateId.js";
3
+ import { DOC_NODE_ID } from "./constants.js";
3
4
  interface NodeParent {
4
5
  nodeId: string;
5
6
  nodeType: string;
@@ -14,9 +15,10 @@ export interface StructureMarkAttrs {
14
15
  op: Op;
15
16
  };
16
17
  }
18
+ export type StructuralContextPath = readonly [string, ...string[]];
17
19
  export interface DocParent extends Omit<NodeParent, "nodeId"> {
18
- nodeId: "__doc__";
19
- nodeType: "__doc__";
20
+ nodeId: typeof DOC_NODE_ID;
21
+ nodeType: typeof DOC_NODE_ID;
20
22
  }
21
23
  export type Parent = NodeParent | DocParent;
22
24
  export interface MoveOp {
@@ -1,5 +1,6 @@
1
+ import { DOC_NODE_ID } from "./constants.js";
1
2
  export function guardDocParent(parent) {
2
- return parent != null && parent.nodeId === "__doc__" && parent.nodeType === "__doc__";
3
+ return parent != null && parent.nodeId === DOC_NODE_ID && parent.nodeType === DOC_NODE_ID;
3
4
  }
4
5
  export function guardStructureMarkAttrs(attrs) {
5
6
  if (!("data" in attrs)) return false;
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export { suggestChanges, suggestChangesKey, isSuggestChangesEnabled, } from "./p
4
4
  export { withSuggestChanges, transformToSuggestionTransaction, } from "./withSuggestChanges.js";
5
5
  export { ensureSelection as experimental_ensureSelection, ensureSelectionKey as experimental_ensureSelectionKey, isEnsureSelectionEnabled as experimental_isEnsureSelectionEnabled, } from "./ensureSelectionPlugin.js";
6
6
  export { guardStructureMarkAttrs } from "./features/wrapUnwrap/types.js";
7
- export type { Op as StructureOp, StructureMarkAttrs, } from "./features/wrapUnwrap/types.js";
7
+ export type { Op as StructureOp, StructureMarkAttrs, StructuralContextPath, } from "./features/wrapUnwrap/types.js";
8
8
  export { wrappingInputRule as experimental_wrappingInputRule } from "./wrappingInputRule.js";
@@ -1,2 +1,2 @@
1
1
  import { Schema } from "prosemirror-model";
2
- export declare function createSchema(deletionMarksVisibility?: "hidden" | "visible"): Schema<"blockquote" | "text" | "orderedList" | "bulletList" | "listItem" | "doc" | "paragraph" | "horizontal_rule" | "heading" | "code_block" | "image" | "hard_break", "insertion" | "deletion" | "modification" | "structure" | "code" | "em" | "link" | "strong">;
2
+ export declare function createSchema(deletionMarksVisibility?: "hidden" | "visible"): Schema<"blockquote" | "text" | "doc" | "paragraph" | "horizontal_rule" | "heading" | "code_block" | "image" | "hard_break" | "orderedList" | "bulletList" | "listItem", "insertion" | "deletion" | "modification" | "structure" | "code" | "em" | "link" | "strong">;
@@ -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
- export declare const schema: Schema<"blockquote" | "text" | "orderedList" | "bulletList" | "listItem" | "doc" | "paragraph" | "horizontal_rule" | "heading" | "code_block" | "image" | "hard_break", "insertion" | "deletion" | "modification" | "structure" | "code" | "em" | "link" | "strong" | "difficulty">;
3
+ export declare const schema: Schema<"blockquote" | "text" | "doc" | "paragraph" | "horizontal_rule" | "heading" | "code_block" | "image" | "hard_break" | "orderedList" | "bulletList" | "listItem", "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
  };
@@ -3,6 +3,7 @@ import { type EditorState, type Transaction } from "prosemirror-state";
3
3
  import { type Transform } from "prosemirror-transform";
4
4
  import { type EditorView } from "prosemirror-view";
5
5
  import { type SuggestionId } from "./generateId.js";
6
+ import { type StructuralContextPath } from "./features/wrapUnwrap/types.js";
6
7
  /**
7
8
  * Given a standard transaction from ProseMirror, produce
8
9
  * a new transaction that tracks the changes from the original,
@@ -27,5 +28,6 @@ export declare function transformToSuggestionTransaction(originalTransaction: Tr
27
28
  */
28
29
  export declare function withSuggestChanges(dispatchTransaction?: EditorView["dispatch"], generateId?: (schema: Schema, doc?: Node) => SuggestionId, opts?: {
29
30
  experimental_trackStructureChanges?: boolean;
31
+ experimental_trackStructures?: StructuralContextPath[];
30
32
  experimental_ensureUniqueNodeIds?: (transactions: Transaction[], oldDoc: Node, newDoc: Node) => Transform;
31
33
  }): EditorView["dispatch"];
@@ -10,7 +10,7 @@ import { isSuggestChangesEnabled, suggestChangesKey } from "./plugin.js";
10
10
  import { generateNextNumberId } from "./generateId.js";
11
11
  import { getSuggestionMarks } from "./utils.js";
12
12
  import { prependDeletionsWithZWSP } from "./prependDeletionsWithZWSP.js";
13
- import { suggestStructureChanges } from "./features/wrapUnwrap/structureChangesPlugin.js";
13
+ import { getRequiredStructuralContextPaths, suggestStructureChanges } from "./features/wrapUnwrap/structureChangesPlugin.js";
14
14
  const TRACE_ENABLED = true;
15
15
  function trace(...args) {
16
16
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -121,13 +121,15 @@ function getStepHandler(step) {
121
121
  if (isEnabled) {
122
122
  let structureChangesResult = null;
123
123
  const docBefore = transaction.docs[0];
124
- if (transaction.docChanged && docBefore && opts?.experimental_trackStructureChanges && typeof opts.experimental_ensureUniqueNodeIds === "function") {
124
+ const structuralContextPaths = opts?.experimental_trackStructureChanges ? getRequiredStructuralContextPaths(opts.experimental_trackStructures) : null;
125
+ const ensureUniqueNodeIds = opts?.experimental_ensureUniqueNodeIds;
126
+ if (transaction.docChanged && docBefore && structuralContextPaths && typeof ensureUniqueNodeIds === "function") {
125
127
  trace("trying to track structure changes first...");
126
128
  // after a transaction, some nodes may not yet have unique ids (they were just added, and the unique id plugin has not yet run)
127
129
  // this hook allows to "post-process" the transaction and add the missing ids
128
130
  // basically it allows to run the core logic of the unique ids plugin earlier
129
131
  const perfUid = performance.now();
130
- const uniqueNodeIdsTransform = opts.experimental_ensureUniqueNodeIds([
132
+ const uniqueNodeIdsTransform = ensureUniqueNodeIds([
131
133
  transaction
132
134
  ], docBefore, transaction.doc);
133
135
  trace("perf", "structure", "ensureUniqueNodsIds took", Number((performance.now() - perfUid).toFixed(2)), "ms");
@@ -137,7 +139,7 @@ function getStepHandler(step) {
137
139
  // if handled, then ignore the main plugin
138
140
  // otherwise use the main plugin
139
141
  const perfStructure = performance.now();
140
- structureChangesResult = suggestStructureChanges(docBefore, docAfter, generateId);
142
+ structureChangesResult = suggestStructureChanges(docBefore, docAfter, structuralContextPaths, generateId);
141
143
  trace("perf", "structure", "suggestStructureChanges took", Number((performance.now() - perfStructure).toFixed(2)), "ms");
142
144
  trace("structure changes transform completed", structureChangesResult.transform);
143
145
  if (structureChangesResult.handled) {
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.12",
3
+ "version": "0.3.3-wrap-unwrap.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",