prosemirror-changeset 2.3.0 → 2.4.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 2.4.0 (2026-02-14)
2
+
3
+ ### New features
4
+
5
+ `Change` objects can now be serialized to and deserialized from JSON, and `ChangeSet.create` allows you to pass in a set of changes.
6
+
7
+ ## 2.3.1 (2025-05-28)
8
+
9
+ ### Bug fixes
10
+
11
+ Improve diffing to not treat closing tokens of different node types as the same token.
12
+
1
13
  ## 2.3.0 (2025-05-05)
2
14
 
3
15
  ### New features
package/README.md CHANGED
@@ -43,11 +43,17 @@ A replaced range with metadata associated with it.
43
43
  Data associated with the inserted content. Length adds up to
44
44
  `this.toB - this.fromB`.
45
45
 
46
+ * **`toJSON`**`() → ChangeJSON`\
47
+ Returns a JSON-serializeable object to represent this change.
48
+
46
49
  * `static `**`merge`**`<Data>(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\
47
50
  This merges two changesets (the end document of x should be the
48
51
  start document of y) into a single one spanning the start of x to
49
52
  the end of y.
50
53
 
54
+ * `static `**`fromJSON`**`<Data>(json: ChangeJSON) → Change`\
55
+ Deserialize a change from JSON format.
56
+
51
57
 
52
58
  ### class Span`<Data = any>`
53
59
 
@@ -96,7 +102,7 @@ partially undo themselves by comparing their content.
96
102
  make sure the method is called on the old set and passed the new
97
103
  set. The returned positions will be in new document coordinates.
98
104
 
99
- * `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any, tokenEncoder?: TokenEncoder = DefaultEncoder) → ChangeSet`\
105
+ * `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any, tokenEncoder?: TokenEncoder = DefaultEncoder, changes?: readonly Change[] = []) → ChangeSet`\
100
106
  Create a changeset with the given base object and configuration.
101
107
 
102
108
  The `combine` function is used to compare and combine metadata—it
@@ -108,6 +114,11 @@ partially undo themselves by comparing their content.
108
114
  changes. The default is to just compare nodes by name and text
109
115
  by character, ignoring marks and attributes.
110
116
 
117
+ To serialize a change set, you can store its document and
118
+ change array as JSON, and then pass the deserialized (via
119
+ [`Change.fromJSON`](#changes.Change^fromJSON)) set of changes
120
+ as fourth argument to `create` to recreate the set.
121
+
111
122
 
112
123
  * **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\
113
124
  Simplifies a set of changes for presentation. This makes the
@@ -144,3 +155,21 @@ performance.
144
155
  Compare the given tokens. Should return true when they count as
145
156
  equal.
146
157
 
158
+
159
+ ### type ChangeJSON`<Data>`
160
+
161
+ JSON-serialized form of a change.
162
+
163
+ * **`fromA`**`: number`
164
+
165
+ * **`toA`**`: number`
166
+
167
+ * **`fromB`**`: number`
168
+
169
+ * **`toB`**`: number`
170
+
171
+ * **`deleted`**`: readonly {length: number, data: Data}[]`
172
+
173
+ * **`inserted`**`: readonly {length: number, data: Data}[]`
174
+
175
+
package/dist/index.cjs CHANGED
@@ -12,6 +12,12 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
12
12
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
13
13
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
14
14
  function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
15
+ function typeID(type) {
16
+ var cache = type.schema.cached.changeSetIDs || (type.schema.cached.changeSetIDs = Object.create(null));
17
+ var id = cache[type.name];
18
+ if (id == null) cache[type.name] = id = Object.keys(type.schema.nodes).indexOf(type.name) + 1;
19
+ return id;
20
+ }
15
21
  var DefaultEncoder = {
16
22
  encodeCharacter: function encodeCharacter(_char) {
17
23
  return _char;
@@ -19,8 +25,8 @@ var DefaultEncoder = {
19
25
  encodeNodeStart: function encodeNodeStart(node) {
20
26
  return node.type.name;
21
27
  },
22
- encodeNodeEnd: function encodeNodeEnd() {
23
- return -1;
28
+ encodeNodeEnd: function encodeNodeEnd(node) {
29
+ return -typeID(node.type);
24
30
  },
25
31
  compareTokens: function compareTokens(a, b) {
26
32
  return a === b;
@@ -204,6 +210,11 @@ var Change = function () {
204
210
  if (startA == 0 && startB == 0 && endA == this.toA - this.fromA && endB == this.toB - this.fromB) return this;
205
211
  return new Change(this.fromA + startA, this.fromA + endA, this.fromB + startB, this.fromB + endB, Span.slice(this.deleted, startA, endA), Span.slice(this.inserted, startB, endB));
206
212
  }
213
+ }, {
214
+ key: "toJSON",
215
+ value: function toJSON() {
216
+ return this;
217
+ }
207
218
  }], [{
208
219
  key: "merge",
209
220
  value: function merge(x, y, combine) {
@@ -270,6 +281,15 @@ var Change = function () {
270
281
  }
271
282
  }
272
283
  }
284
+ }, {
285
+ key: "fromJSON",
286
+ value: function fromJSON(json) {
287
+ return new Change(json.fromA, json.toA, json.fromB, json.toB, json.deleted.map(function (d) {
288
+ return new Span(d.length, d.data);
289
+ }), json.inserted.map(function (d) {
290
+ return new Span(d.length, d.data);
291
+ }));
292
+ }
273
293
  }]);
274
294
  return Change;
275
295
  }();
@@ -501,11 +521,12 @@ var ChangeSet = function () {
501
521
  return a === b ? a : null;
502
522
  };
503
523
  var tokenEncoder = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DefaultEncoder;
524
+ var changes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
504
525
  return new ChangeSet({
505
526
  combine: combine,
506
527
  doc: doc,
507
528
  encoder: tokenEncoder
508
- }, []);
529
+ }, changes);
509
530
  }
510
531
  }]);
511
532
  return ChangeSet;
package/dist/index.d.cts CHANGED
@@ -50,7 +50,32 @@ declare class Change<Data = any> {
50
50
  the end of y.
51
51
  */
52
52
  static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
+ /**
54
+ Deserialize a change from JSON format.
55
+ */
56
+ static fromJSON<Data>(json: ChangeJSON<Data>): Change<Data>;
57
+ /**
58
+ Returns a JSON-serializeable object to represent this change.
59
+ */
60
+ toJSON(): ChangeJSON<Data>;
53
61
  }
62
+ /**
63
+ JSON-serialized form of a change.
64
+ */
65
+ type ChangeJSON<Data> = {
66
+ fromA: number;
67
+ toA: number;
68
+ fromB: number;
69
+ toB: number;
70
+ deleted: readonly {
71
+ length: number;
72
+ data: Data;
73
+ }[];
74
+ inserted: readonly {
75
+ length: number;
76
+ data: Data;
77
+ }[];
78
+ };
54
79
 
55
80
  /**
56
81
  A token encoder can be passed when creating a `ChangeSet` in order
@@ -149,8 +174,13 @@ declare class ChangeSet<Data = any> {
149
174
  serialized and compared when diffing the content produced by
150
175
  changes. The default is to just compare nodes by name and text
151
176
  by character, ignoring marks and attributes.
177
+
178
+ To serialize a change set, you can store its document and
179
+ change array as JSON, and then pass the deserialized (via
180
+ [`Change.fromJSON`](https://prosemirror.net/docs/ref/#changes.Change^fromJSON)) set of changes
181
+ as fourth argument to `create` to recreate the set.
152
182
  */
153
- static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
183
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>, changes?: readonly Change<Data>[]): ChangeSet<Data>;
154
184
  }
155
185
 
156
- export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
186
+ export { Change, type ChangeJSON, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.d.ts CHANGED
@@ -50,7 +50,32 @@ declare class Change<Data = any> {
50
50
  the end of y.
51
51
  */
52
52
  static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
+ /**
54
+ Deserialize a change from JSON format.
55
+ */
56
+ static fromJSON<Data>(json: ChangeJSON<Data>): Change<Data>;
57
+ /**
58
+ Returns a JSON-serializeable object to represent this change.
59
+ */
60
+ toJSON(): ChangeJSON<Data>;
53
61
  }
62
+ /**
63
+ JSON-serialized form of a change.
64
+ */
65
+ type ChangeJSON<Data> = {
66
+ fromA: number;
67
+ toA: number;
68
+ fromB: number;
69
+ toB: number;
70
+ deleted: readonly {
71
+ length: number;
72
+ data: Data;
73
+ }[];
74
+ inserted: readonly {
75
+ length: number;
76
+ data: Data;
77
+ }[];
78
+ };
54
79
 
55
80
  /**
56
81
  A token encoder can be passed when creating a `ChangeSet` in order
@@ -149,8 +174,13 @@ declare class ChangeSet<Data = any> {
149
174
  serialized and compared when diffing the content produced by
150
175
  changes. The default is to just compare nodes by name and text
151
176
  by character, ignoring marks and attributes.
177
+
178
+ To serialize a change set, you can store its document and
179
+ change array as JSON, and then pass the deserialized (via
180
+ [`Change.fromJSON`](https://prosemirror.net/docs/ref/#changes.Change^fromJSON)) set of changes
181
+ as fourth argument to `create` to recreate the set.
152
182
  */
153
- static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
183
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>, changes?: readonly Change<Data>[]): ChangeSet<Data>;
154
184
  }
155
185
 
156
- export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
186
+ export { Change, type ChangeJSON, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
+ function typeID(type) {
2
+ let cache = type.schema.cached.changeSetIDs || (type.schema.cached.changeSetIDs = Object.create(null));
3
+ let id = cache[type.name];
4
+ if (id == null)
5
+ cache[type.name] = id = Object.keys(type.schema.nodes).indexOf(type.name) + 1;
6
+ return id;
7
+ }
8
+ // The default token encoder, which encodes node open tokens are
9
+ // encoded as strings holding the node name, characters as their
10
+ // character code, and node close tokens as negative numbers.
1
11
  const DefaultEncoder = {
2
12
  encodeCharacter: char => char,
3
13
  encodeNodeStart: node => node.type.name,
4
- encodeNodeEnd: () => -1,
14
+ encodeNodeEnd: node => -typeID(node.type),
5
15
  compareTokens: (a, b) => a === b
6
16
  };
7
- // Convert the given range of a fragment to tokens, where node open
8
- // tokens are encoded as strings holding the node name, characters as
9
- // their character code, and node close tokens as -1.
17
+ // Convert the given range of a fragment to tokens.
10
18
  function tokens(frag, encoder, start, end, target) {
11
19
  for (let i = 0, off = 0; i < frag.childCount; i++) {
12
20
  let child = frag.child(i), endOff = off + child.nodeSize;
@@ -342,6 +350,16 @@ class Change {
342
350
  }
343
351
  }
344
352
  }
353
+ /**
354
+ Deserialize a change from JSON format.
355
+ */
356
+ static fromJSON(json) {
357
+ return new Change(json.fromA, json.toA, json.fromB, json.toB, json.deleted.map(d => new Span(d.length, d.data)), json.inserted.map(d => new Span(d.length, d.data)));
358
+ }
359
+ /**
360
+ Returns a JSON-serializeable object to represent this change.
361
+ */
362
+ toJSON() { return this; }
345
363
  }
346
364
 
347
365
  let letter;
@@ -638,9 +656,14 @@ class ChangeSet {
638
656
  serialized and compared when diffing the content produced by
639
657
  changes. The default is to just compare nodes by name and text
640
658
  by character, ignoring marks and attributes.
659
+
660
+ To serialize a change set, you can store its document and
661
+ change array as JSON, and then pass the deserialized (via
662
+ [`Change.fromJSON`](https://prosemirror.net/docs/ref/#changes.Change^fromJSON)) set of changes
663
+ as fourth argument to `create` to recreate the set.
641
664
  */
642
- static create(doc, combine = (a, b) => a === b ? a : null, tokenEncoder = DefaultEncoder) {
643
- return new ChangeSet({ combine, doc, encoder: tokenEncoder }, []);
665
+ static create(doc, combine = (a, b) => a === b ? a : null, tokenEncoder = DefaultEncoder, changes = []) {
666
+ return new ChangeSet({ combine, doc, encoder: tokenEncoder }, changes);
644
667
  }
645
668
  }
646
669
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-changeset",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Distills a series of editing steps into deleted and added ranges",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/README.md CHANGED
@@ -27,4 +27,6 @@ it was made, or the step data necessary to invert it again.
27
27
 
28
28
  @simplifyChanges
29
29
 
30
- @TokenEncoder
30
+ @TokenEncoder
31
+
32
+ @ChangeJSON
package/src/change.ts CHANGED
@@ -168,4 +168,22 @@ export class Change<Data = any> {
168
168
  }
169
169
  }
170
170
  }
171
+
172
+ /// Deserialize a change from JSON format.
173
+ static fromJSON<Data>(json: ChangeJSON<Data>) {
174
+ return new Change(json.fromA, json.toA, json.fromB, json.toB,
175
+ json.deleted.map(d => new Span(d.length, d.data)),
176
+ json.inserted.map(d => new Span(d.length, d.data)))
177
+ }
178
+
179
+ /// Returns a JSON-serializeable object to represent this change.
180
+ toJSON(): ChangeJSON<Data> { return this }
181
+ }
182
+
183
+ /// JSON-serialized form of a change.
184
+ export type ChangeJSON<Data> = {
185
+ fromA: number, toA: number,
186
+ fromB: number, toB: number,
187
+ deleted: readonly {length: number, data: Data}[],
188
+ inserted: readonly {length: number, data: Data}[]
171
189
  }
package/src/changeset.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import {Node} from "prosemirror-model"
2
2
  import {StepMap} from "prosemirror-transform"
3
3
  import {computeDiff, TokenEncoder, DefaultEncoder} from "./diff"
4
- import {Change, Span} from "./change"
5
- export {Change, Span}
4
+ import {Change, Span, ChangeJSON} from "./change"
5
+ export {Change, Span, ChangeJSON}
6
6
  export {simplifyChanges} from "./simplify"
7
7
  export {TokenEncoder}
8
8
 
@@ -146,12 +146,18 @@ export class ChangeSet<Data = any> {
146
146
  /// serialized and compared when diffing the content produced by
147
147
  /// changes. The default is to just compare nodes by name and text
148
148
  /// by character, ignoring marks and attributes.
149
+ ///
150
+ /// To serialize a change set, you can store its document and
151
+ /// change array as JSON, and then pass the deserialized (via
152
+ /// [`Change.fromJSON`](#changes.Change^fromJSON)) set of changes
153
+ /// as fourth argument to `create` to recreate the set.
149
154
  static create<Data = any>(
150
155
  doc: Node,
151
156
  combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any,
152
- tokenEncoder: TokenEncoder<any> = DefaultEncoder
157
+ tokenEncoder: TokenEncoder<any> = DefaultEncoder,
158
+ changes: readonly Change<Data>[] = []
153
159
  ) {
154
- return new ChangeSet({combine, doc, encoder: tokenEncoder}, [])
160
+ return new ChangeSet({combine, doc, encoder: tokenEncoder}, changes)
155
161
  }
156
162
 
157
163
  /// Exported for testing @internal
package/src/diff.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {Fragment, Node, Mark} from "prosemirror-model"
1
+ import {Fragment, Node, NodeType, Mark} from "prosemirror-model"
2
2
  import {Change} from "./change"
3
3
 
4
4
  /// A token encoder can be passed when creating a `ChangeSet` in order
@@ -23,16 +23,24 @@ export interface TokenEncoder<T> {
23
23
  compareTokens(a: T, b: T): boolean
24
24
  }
25
25
 
26
+ function typeID(type: NodeType) {
27
+ let cache: Record<string, number> = type.schema.cached.changeSetIDs || (type.schema.cached.changeSetIDs = Object.create(null))
28
+ let id = cache[type.name]
29
+ if (id == null) cache[type.name] = id = Object.keys(type.schema.nodes).indexOf(type.name) + 1
30
+ return id
31
+ }
32
+
33
+ // The default token encoder, which encodes node open tokens are
34
+ // encoded as strings holding the node name, characters as their
35
+ // character code, and node close tokens as negative numbers.
26
36
  export const DefaultEncoder: TokenEncoder<number | string> = {
27
37
  encodeCharacter: char => char,
28
38
  encodeNodeStart: node => node.type.name,
29
- encodeNodeEnd: () => -1,
39
+ encodeNodeEnd: node => -typeID(node.type),
30
40
  compareTokens: (a, b) => a === b
31
41
  }
32
42
 
33
- // Convert the given range of a fragment to tokens, where node open
34
- // tokens are encoded as strings holding the node name, characters as
35
- // their character code, and node close tokens as -1.
43
+ // Convert the given range of a fragment to tokens.
36
44
  function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end: number, target: T[]) {
37
45
  for (let i = 0, off = 0; i < frag.childCount; i++) {
38
46
  let child = frag.child(i), endOff = off + child.nodeSize
package/test/test-diff.ts CHANGED
@@ -63,4 +63,7 @@ describe("computeDiff", () => {
63
63
 
64
64
  it("can handle ambiguous diffs", () =>
65
65
  test(doc(p("abcbcd")), doc(p("abcd")), [4, 6, 4, 4]))
66
+
67
+ it("sees the difference between different closing tokens", () =>
68
+ test(doc(p("a")), doc(h1("oo")), [0, 3, 0, 4]))
66
69
  })