Skip to content

Commit b62ed45

Browse files
jevakallioVV-EEsmoores-dev
authored
Support custom id generation (#13)
--------- Co-authored-by: VV-EE <67345012+VV-EE@users.noreply.github.com> Co-authored-by: Shane Friedman <smoores-gpg@friedmans.us>
1 parent cff0b1d commit b62ed45

15 files changed

Lines changed: 172 additions & 55 deletions

.yarn/versions/04b5cd81.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
releases:
2+
"@handlewithcare/prosemirror-suggest-changes": patch

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,5 +384,10 @@ document.
384384
```ts
385385
function withSuggestChanges(
386386
dispatchTransaction?: EditorView["dispatch"],
387+
generateId?: (schema: Schema, doc?: Node) => SuggestionId,
387388
): EditorView["dispatch"];
388389
```
390+
391+
`generateId` can be used to customize the unique ids assigned to suggestion
392+
marks. If undefined, the default implementation (an auto-incrementing integer)
393+
will be used.

src/__tests__/replaceAroundStep.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,6 @@ describe("ReplaceAroundStep", () => {
114114
{
115115
id: 1,
116116
type: "mark",
117-
// we know this mark exists
118-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
119117
previousValue: doc.children[0]!.marks[0]!.toJSON(),
120118
newValue: null,
121119
},

src/__tests__/withSuggestChanges.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
12
/* eslint-disable @typescript-eslint/no-non-null-assertion */
3+
import { eq } from "prosemirror-test-builder";
4+
import { type SuggestionId } from "../generateId.js";
25
import { EditorState } from "prosemirror-state";
36
import { Fragment, Slice } from "prosemirror-model";
47
import { type ReplaceStep, replaceStep } from "prosemirror-transform";
@@ -157,4 +160,78 @@ describe("withSuggestChanges", () => {
157160
// Should pass without error - document should be modified
158161
expect(newState.doc.toJSON()).not.toEqual(doc.toJSON());
159162
});
163+
it("should use custom generateId function when provided", () => {
164+
const doc = testBuilders.doc(testBuilders.paragraph("Hello world"));
165+
166+
let state = EditorState.create({
167+
doc,
168+
});
169+
170+
let callCount = 0;
171+
const customIds = ["custom-id-1", "custom-id-2"];
172+
173+
const generateId = (): SuggestionId => {
174+
const id = customIds[callCount] ?? `custom-id-${callCount + 1}`;
175+
callCount++;
176+
return id;
177+
};
178+
179+
// Make an insertion after "Hello"
180+
const tr1 = state.tr.insertText(" there", 6);
181+
const suggestedTr1 = transformToSuggestionTransaction(
182+
tr1,
183+
state,
184+
generateId,
185+
);
186+
state = state.apply(suggestedTr1);
187+
188+
// Make another insertion at the end
189+
const tr2 = state.tr.insertText("!", state.doc.content.size - 1);
190+
const suggestedTr2 = transformToSuggestionTransaction(
191+
tr2,
192+
state,
193+
generateId,
194+
);
195+
state = state.apply(suggestedTr2);
196+
197+
const expected = testBuilders.doc(
198+
testBuilders.paragraph(
199+
"Hello",
200+
testBuilders.insertion({ id: customIds[0] }, " there"),
201+
" world",
202+
testBuilders.insertion({ id: customIds[1] }, "!"),
203+
),
204+
);
205+
206+
assert(
207+
eq(state.doc, expected),
208+
`Expected ${state.doc} to match ${expected}`,
209+
);
210+
});
211+
212+
it("should use default numeric ID generation when generateId is not provided", () => {
213+
const doc = testBuilders.doc(testBuilders.paragraph("Hello world"));
214+
215+
let state = EditorState.create({
216+
doc,
217+
});
218+
219+
// Make an insertion without providing generateId
220+
const tr = state.tr.insertText(" there", 6);
221+
const suggestedTr = transformToSuggestionTransaction(tr, state); // No generateId
222+
state = state.apply(suggestedTr);
223+
224+
const expected = testBuilders.doc(
225+
testBuilders.paragraph(
226+
"Hello",
227+
testBuilders.insertion({ id: 1 }, " there"),
228+
" world",
229+
),
230+
);
231+
232+
assert(
233+
eq(state.doc, expected),
234+
`Expected ${state.doc} to match ${expected}`,
235+
);
236+
});
160237
});

src/addMarkStep.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import { applySuggestionsToRange } from "./commands.js";
1111
import { suggestReplaceStep } from "./replaceStep.js";
12+
import { type SuggestionId } from "./generateId.js";
1213

1314
/**
1415
* Transform an add mark step into its equivalent tracked steps.
@@ -23,7 +24,7 @@ export function trackAddMarkStep(
2324
doc: Node,
2425
step: AddMarkStep,
2526
prevSteps: Step[],
26-
suggestionId: number,
27+
suggestionId: SuggestionId,
2728
) {
2829
const applied = step.apply(doc).doc;
2930
if (!applied) return false;

src/addNodeMarkStep.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type AddNodeMarkStep, type Step } from "prosemirror-transform";
44

55
import { rebasePos } from "./rebasePos.js";
66
import { getSuggestionMarks } from "./utils.js";
7+
import { type SuggestionId } from "./generateId.js";
78

89
/**
910
* Transform an add node mark step into its equivalent tracked steps.
@@ -17,7 +18,7 @@ export function trackAddNodeMarkStep(
1718
_doc: Node,
1819
step: AddNodeMarkStep,
1920
prevSteps: Step[],
20-
suggestionId: number,
21+
suggestionId: SuggestionId,
2122
) {
2223
const { modification } = getSuggestionMarks(state.schema);
2324

src/attrStep.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type AttrStep, type Step } from "prosemirror-transform";
44

55
import { rebasePos } from "./rebasePos.js";
66
import { getSuggestionMarks } from "./utils.js";
7+
import { type SuggestionId } from "./generateId.js";
78

89
/**
910
* Transform an attr mark step into its equivalent tracked steps.
@@ -17,7 +18,7 @@ export function trackAttrStep(
1718
_doc: Node,
1819
step: AttrStep,
1920
prevSteps: Step[],
20-
suggestionId: number,
21+
suggestionId: SuggestionId,
2122
) {
2223
const { modification } = getSuggestionMarks(state.schema);
2324

src/commands.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { type EditorView } from "prosemirror-view";
1010
import { findSuggestionMarkEnd } from "./findSuggestionMarkEnd.js";
1111
import { suggestChangesKey } from "./plugin.js";
1212
import { getSuggestionMarks } from "./utils.js";
13+
import { type SuggestionId } from "./generateId.js";
1314

1415
/**
1516
* Given a node and a transform, add a set of steps to the
@@ -24,7 +25,7 @@ function applySuggestionsToTransform(
2425
tr: Transform,
2526
markTypeToApply: MarkType,
2627
markTypeToRevert: MarkType,
27-
suggestionId?: number,
28+
suggestionId?: SuggestionId,
2829
from?: number,
2930
to?: number,
3031
) {
@@ -144,7 +145,7 @@ function revertModifications(node: Node, pos: number, tr: Transform) {
144145

145146
function modificationIsInSet(
146147
modification: MarkType,
147-
id: number | undefined,
148+
id: SuggestionId | undefined,
148149
marks: readonly Mark[],
149150
) {
150151
const mark = modification.isInSet(marks);
@@ -159,7 +160,7 @@ function applyModificationsToTransform(
159160
node: Node,
160161
tr: Transform,
161162
dir: number,
162-
suggestionId?: number,
163+
suggestionId?: SuggestionId,
163164
from?: number,
164165
to?: number,
165166
) {
@@ -243,6 +244,7 @@ export function applySuggestionsToRange(doc: Node, from: number, to: number) {
243244
doc,
244245
transform,
245246
1,
247+
undefined,
246248
nodeRange.start,
247249
nodeRange.end,
248250
);
@@ -281,7 +283,7 @@ export function applySuggestions(
281283
* The insertion mark and modification mark will be removed, and their
282284
* contents left in the doc.
283285
*/
284-
export function applySuggestion(suggestionId: number): Command {
286+
export function applySuggestion(suggestionId: SuggestionId): Command {
285287
return (state, dispatch) => {
286288
const { deletion, insertion } = getSuggestionMarks(state.schema);
287289

@@ -328,7 +330,7 @@ export function revertSuggestions(
328330
* The deletion mark will be removed, and their contents left in the doc.
329331
* Modifications tracked in modification marks will be reverted.
330332
*/
331-
export function revertSuggestion(suggestionId: number): Command {
333+
export function revertSuggestion(suggestionId: SuggestionId): Command {
332334
return (state, dispatch) => {
333335
const { deletion, insertion } = getSuggestionMarks(state.schema);
334336

@@ -351,7 +353,7 @@ export function revertSuggestion(suggestionId: number): Command {
351353
/**
352354
* Command that updates the selection to cover an existing change.
353355
*/
354-
export function selectSuggestion(suggestionId: number): Command {
356+
export function selectSuggestion(suggestionId: SuggestionId): Command {
355357
return (state, dispatch) => {
356358
const { deletion, insertion, modification } = getSuggestionMarks(
357359
state.schema,

src/generateId.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type Node, type Schema } from "prosemirror-model";
2+
import { getSuggestionMarks } from "./utils.js";
3+
4+
export type SuggestionId = string | number;
5+
6+
export const suggestionIdValidate = "number|string";
7+
8+
export function parseSuggestionId(id: string): SuggestionId {
9+
const parsed = parseInt(id, 10);
10+
if (isNaN(parsed)) {
11+
return id;
12+
}
13+
return parsed;
14+
}
15+
16+
export function generateNextNumberId(schema: Schema, doc?: Node) {
17+
const { deletion, insertion, modification } = getSuggestionMarks(schema);
18+
// Find the highest change id in the document so far,
19+
// and use that as the starting point for new changes
20+
let suggestionId = 0;
21+
doc?.descendants((node) => {
22+
const mark = node.marks.find(
23+
(mark) =>
24+
mark.type === insertion ||
25+
mark.type === deletion ||
26+
mark.type === modification,
27+
);
28+
if (mark) {
29+
suggestionId = Math.max(suggestionId, mark.attrs["id"] as number);
30+
return false;
31+
}
32+
return true;
33+
});
34+
return suggestionId + 1;
35+
}

src/removeMarkStep.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import { applySuggestionsToRange } from "./commands.js";
1111
import { suggestReplaceStep } from "./replaceStep.js";
12+
import { type SuggestionId } from "./generateId.js";
1213

1314
/**
1415
* Transform a remove mark step into its equivalent tracked steps.
@@ -23,7 +24,7 @@ export function suggestRemoveMarkStep(
2324
doc: Node,
2425
step: RemoveMarkStep,
2526
prevSteps: Step[],
26-
suggestionId: number,
27+
suggestionId: SuggestionId,
2728
) {
2829
const applied = step.apply(doc).doc;
2930
if (!applied) return false;

0 commit comments

Comments
 (0)