prosemirror-changeset 2.2.1 → 2.3.1

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.3.1 (2025-05-28)
2
+
3
+ ### Bug fixes
4
+
5
+ Improve diffing to not treat closing tokens of different node types as the same token.
6
+
7
+ ## 2.3.0 (2025-05-05)
8
+
9
+ ### New features
10
+
11
+ Change sets can now be passed a custom token encoder that controls the way changed content is diffed.
12
+
1
13
  ## 2.2.1 (2023-05-17)
2
14
 
3
15
  ### Bug fixes
package/README.md CHANGED
@@ -41,7 +41,7 @@ A replaced range with metadata associated with it.
41
41
 
42
42
  * **`inserted`**`: readonly Span[]`\
43
43
  Data associated with the inserted content. Length adds up to
44
- `this.toB - this.toA`.
44
+ `this.toB - this.fromB`.
45
45
 
46
46
  * `static `**`merge`**`<Data>(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\
47
47
  This merges two changesets (the end document of x should be the
@@ -96,12 +96,18 @@ partially undo themselves by comparing their content.
96
96
  make sure the method is called on the old set and passed the new
97
97
  set. The returned positions will be in new document coordinates.
98
98
 
99
- * `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any) → ChangeSet`\
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`\
100
100
  Create a changeset with the given base object and configuration.
101
+
101
102
  The `combine` function is used to compare and combine metadata—it
102
103
  should return null when metadata isn't compatible, and a combined
103
104
  version for a merged range when it is.
104
105
 
106
+ When given, a token encoder determines how document tokens are
107
+ serialized and compared when diffing the content produced by
108
+ changes. The default is to just compare nodes by name and text
109
+ by character, ignoring marks and attributes.
110
+
105
111
 
106
112
  * **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\
107
113
  Simplifies a set of changes for presentation. This makes the
@@ -111,3 +117,30 @@ partially undo themselves by comparing their content.
111
117
  words (in the new document) they touch. An exception is made for
112
118
  single-character replacements.
113
119
 
120
+
121
+ ### interface TokenEncoder`<T>`
122
+
123
+ A token encoder can be passed when creating a `ChangeSet` in order
124
+ to influence the way the library runs its diffing algorithm. The
125
+ encoder determines how document tokens (such as nodes and
126
+ characters) are encoded and compared.
127
+
128
+ Note that both the encoding and the comparison may run a lot, and
129
+ doing non-trivial work in these functions could impact
130
+ performance.
131
+
132
+ * **`encodeCharacter`**`(char: number, marks: readonly Mark[]) → T`\
133
+ Encode a given character, with the given marks applied.
134
+
135
+ * **`encodeNodeStart`**`(node: Node) → T`\
136
+ Encode the start of a node or, if this is a leaf node, the
137
+ entire node.
138
+
139
+ * **`encodeNodeEnd`**`(node: Node) → T`\
140
+ Encode the end token for the given node. It is valid to encode
141
+ every end token in the same way.
142
+
143
+ * **`compareTokens`**`(a: T, b: T) → boolean`\
144
+ Compare the given tokens. Should return true when they count as
145
+ equal.
146
+
package/dist/index.cjs CHANGED
@@ -1,113 +1,96 @@
1
1
  'use strict';
2
2
 
3
3
  function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
4
-
5
4
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
6
-
7
5
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
8
-
9
6
  function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
10
-
11
7
  function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
12
-
13
- function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
14
-
8
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
9
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
15
10
  function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
16
-
17
- function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
18
-
11
+ function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
19
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; }
20
-
21
- function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
22
-
23
- Object.defineProperty(exports, '__esModule', {
24
- value: true
25
- });
26
-
27
- function tokens(frag, start, end, target) {
13
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
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
+ }
21
+ var DefaultEncoder = {
22
+ encodeCharacter: function encodeCharacter(_char) {
23
+ return _char;
24
+ },
25
+ encodeNodeStart: function encodeNodeStart(node) {
26
+ return node.type.name;
27
+ },
28
+ encodeNodeEnd: function encodeNodeEnd(node) {
29
+ return -typeID(node.type);
30
+ },
31
+ compareTokens: function compareTokens(a, b) {
32
+ return a === b;
33
+ }
34
+ };
35
+ function tokens(frag, encoder, start, end, target) {
28
36
  for (var i = 0, off = 0; i < frag.childCount; i++) {
29
37
  var child = frag.child(i),
30
- endOff = off + child.nodeSize;
38
+ endOff = off + child.nodeSize;
31
39
  var from = Math.max(off, start),
32
- to = Math.min(endOff, end);
33
-
40
+ to = Math.min(endOff, end);
34
41
  if (from < to) {
35
42
  if (child.isText) {
36
- for (var j = from; j < to; j++) {
37
- target.push(child.text.charCodeAt(j - off));
38
- }
43
+ for (var j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text.charCodeAt(j - off), child.marks));
39
44
  } else if (child.isLeaf) {
40
- target.push(child.type.name);
45
+ target.push(encoder.encodeNodeStart(child));
41
46
  } else {
42
- if (from == off) target.push(child.type.name);
43
- tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
44
- if (to == endOff) target.push(-1);
47
+ if (from == off) target.push(encoder.encodeNodeStart(child));
48
+ tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
49
+ if (to == endOff) target.push(encoder.encodeNodeEnd(child));
45
50
  }
46
51
  }
47
-
48
52
  off = endOff;
49
53
  }
50
-
51
54
  return target;
52
55
  }
53
-
54
56
  var MAX_DIFF_SIZE = 5000;
55
-
56
57
  function minUnchanged(sizeA, sizeB) {
57
58
  return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
58
59
  }
59
-
60
60
  function computeDiff(fragA, fragB, range) {
61
- var tokA = tokens(fragA, range.fromA, range.toA, []);
62
- var tokB = tokens(fragB, range.fromB, range.toB, []);
61
+ var encoder = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DefaultEncoder;
62
+ var tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
63
+ var tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
63
64
  var start = 0,
64
- endA = tokA.length,
65
- endB = tokB.length;
66
-
67
- while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start]) {
68
- start++;
69
- }
70
-
65
+ endA = tokA.length,
66
+ endB = tokB.length;
67
+ var cmp = encoder.compareTokens;
68
+ while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++;
71
69
  if (start == tokA.length && start == tokB.length) return [];
72
-
73
- while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1]) {
74
- endA--, endB--;
75
- }
76
-
70
+ while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--;
77
71
  if (endA == start || endB == start || endA == endB && endA == start + 1) return [range.slice(start, endA, start, endB)];
78
72
  var lenA = endA - start,
79
- lenB = endB - start;
73
+ lenB = endB - start;
80
74
  var max = Math.min(MAX_DIFF_SIZE, lenA + lenB),
81
- off = max + 1;
75
+ off = max + 1;
82
76
  var history = [];
83
77
  var frontier = [];
84
-
85
- for (var len = off * 2, i = 0; i < len; i++) {
86
- frontier[i] = -1;
87
- }
88
-
78
+ for (var len = off * 2, i = 0; i < len; i++) frontier[i] = -1;
89
79
  for (var size = 0; size <= max; size++) {
90
- for (var diag = -size; diag <= size; diag += 2) {
91
- var next = frontier[diag + 1 + max],
92
- prev = frontier[diag - 1 + max];
93
- var x = next < prev ? prev : next + 1,
94
- y = x + diag;
95
-
96
- while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y]) {
97
- x++, y++;
98
- }
99
-
100
- frontier[diag + max] = x;
101
-
102
- if (x >= lenA && y >= lenB) {
103
- var _ret = function () {
80
+ var _loop = function _loop(_diag) {
81
+ var next = frontier[_diag + 1 + max],
82
+ prev = frontier[_diag - 1 + max];
83
+ var x = next < prev ? prev : next + 1,
84
+ y = x + _diag;
85
+ while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++;
86
+ frontier[_diag + max] = x;
87
+ if (x >= lenA && y >= lenB) {
104
88
  var diff = [],
105
- minSpan = minUnchanged(endA - start, endB - start);
89
+ minSpan = minUnchanged(endA - start, endB - start);
106
90
  var fromA = -1,
107
- toA = -1,
108
- fromB = -1,
109
- toB = -1;
110
-
91
+ toA = -1,
92
+ fromB = -1,
93
+ toB = -1;
111
94
  var add = function add(fA, tA, fB, tB) {
112
95
  if (fromA > -1 && fromA < tA + minSpan) {
113
96
  fromA = fA;
@@ -120,50 +103,44 @@ function computeDiff(fragA, fragB, range) {
120
103
  toB = tB;
121
104
  }
122
105
  };
123
-
124
106
  for (var _i = size - 1; _i >= 0; _i--) {
125
- var _next = frontier[diag + 1 + max],
126
- _prev = frontier[diag - 1 + max];
127
-
107
+ var _next = frontier[_diag + 1 + max],
108
+ _prev = frontier[_diag - 1 + max];
128
109
  if (_next < _prev) {
129
- diag--;
110
+ _diag--;
130
111
  x = _prev + start;
131
- y = x + diag;
112
+ y = x + _diag;
132
113
  add(x, x, y, y + 1);
133
114
  } else {
134
- diag++;
115
+ _diag++;
135
116
  x = _next + start;
136
- y = x + diag;
117
+ y = x + _diag;
137
118
  add(x, x + 1, y, y);
138
119
  }
139
-
140
120
  frontier = history[_i >> 1];
141
121
  }
142
-
143
122
  if (fromA > -1) diff.push(range.slice(fromA, toA, fromB, toB));
144
123
  return {
145
124
  v: diff.reverse()
146
125
  };
147
- }();
148
-
149
- if (_typeof(_ret) === "object") return _ret.v;
150
- }
126
+ }
127
+ diag = _diag;
128
+ },
129
+ _ret;
130
+ for (var diag = -size; diag <= size; diag += 2) {
131
+ _ret = _loop(diag);
132
+ if (_ret) return _ret.v;
151
133
  }
152
-
153
134
  if (size % 2 == 0) history.push(frontier.slice());
154
135
  }
155
-
156
136
  return [range.slice(start, endA, start, endB)];
157
137
  }
158
-
159
138
  var Span = function () {
160
139
  function Span(length, data) {
161
140
  _classCallCheck(this, Span);
162
-
163
141
  this.length = length;
164
142
  this.data = data;
165
143
  }
166
-
167
144
  _createClass(Span, [{
168
145
  key: "cut",
169
146
  value: function cut(length) {
@@ -175,15 +152,13 @@ var Span = function () {
175
152
  if (from == to) return Span.none;
176
153
  if (from == 0 && to == Span.len(spans)) return spans;
177
154
  var result = [];
178
-
179
155
  for (var i = 0, off = 0; off < to; i++) {
180
156
  var span = spans[i],
181
- end = off + span.length;
157
+ end = off + span.length;
182
158
  var overlap = Math.min(to, end) - Math.max(from, off);
183
159
  if (overlap > 0) result.push(span.cut(overlap));
184
160
  off = end;
185
161
  }
186
-
187
162
  return result;
188
163
  }
189
164
  }, {
@@ -195,35 +170,23 @@ var Span = function () {
195
170
  if (combined == null) return a.concat(b);
196
171
  var result = a.slice(0, a.length - 1);
197
172
  result.push(new Span(a[a.length - 1].length + b[0].length, combined));
198
-
199
- for (var i = 1; i < b.length; i++) {
200
- result.push(b[i]);
201
- }
202
-
173
+ for (var i = 1; i < b.length; i++) result.push(b[i]);
203
174
  return result;
204
175
  }
205
176
  }, {
206
177
  key: "len",
207
178
  value: function len(spans) {
208
179
  var len = 0;
209
-
210
- for (var i = 0; i < spans.length; i++) {
211
- len += spans[i].length;
212
- }
213
-
180
+ for (var i = 0; i < spans.length; i++) len += spans[i].length;
214
181
  return len;
215
182
  }
216
183
  }]);
217
-
218
184
  return Span;
219
185
  }();
220
-
221
186
  Span.none = [];
222
-
223
187
  var Change = function () {
224
188
  function Change(fromA, toA, fromB, toB, deleted, inserted) {
225
189
  _classCallCheck(this, Change);
226
-
227
190
  this.fromA = fromA;
228
191
  this.toA = toA;
229
192
  this.fromB = fromB;
@@ -231,7 +194,6 @@ var Change = function () {
231
194
  this.deleted = deleted;
232
195
  this.inserted = inserted;
233
196
  }
234
-
235
197
  _createClass(Change, [{
236
198
  key: "lenA",
237
199
  get: function get() {
@@ -254,7 +216,6 @@ var Change = function () {
254
216
  if (x.length == 0) return y;
255
217
  if (y.length == 0) return x;
256
218
  var result = [];
257
-
258
219
  for (var iX = 0, iY = 0, curX = x[0], curY = y[0];;) {
259
220
  if (!curX && !curY) {
260
221
  return result;
@@ -264,97 +225,79 @@ var Change = function () {
264
225
  curX = iX++ == x.length ? null : x[iX];
265
226
  } else if (curY && (!curX || curY.toA < curX.fromB)) {
266
227
  var _off = iX ? x[iX - 1].toB - x[iX - 1].toA : 0;
267
-
268
228
  result.push(_off == 0 ? curY : new Change(curY.fromA - _off, curY.toA - _off, curY.fromB, curY.toB, curY.deleted, curY.inserted));
269
229
  curY = iY++ == y.length ? null : y[iY];
270
230
  } else {
271
231
  var pos = Math.min(curX.fromB, curY.fromA);
272
232
  var fromA = Math.min(curX.fromA, curY.fromA - (iX ? x[iX - 1].toB - x[iX - 1].toA : 0)),
273
- toA = fromA;
233
+ toA = fromA;
274
234
  var fromB = Math.min(curY.fromB, curX.fromB + (iY ? y[iY - 1].toB - y[iY - 1].toA : 0)),
275
- toB = fromB;
235
+ toB = fromB;
276
236
  var deleted = Span.none,
277
- inserted = Span.none;
237
+ inserted = Span.none;
278
238
  var enteredX = false,
279
- enteredY = false;
280
-
239
+ enteredY = false;
281
240
  for (;;) {
282
241
  var nextX = !curX ? 2e8 : pos >= curX.fromB ? curX.toB : curX.fromB;
283
242
  var nextY = !curY ? 2e8 : pos >= curY.fromA ? curY.toA : curY.fromA;
284
243
  var next = Math.min(nextX, nextY);
285
244
  var inX = curX && pos >= curX.fromB,
286
- inY = curY && pos >= curY.fromA;
245
+ inY = curY && pos >= curY.fromA;
287
246
  if (!inX && !inY) break;
288
-
289
247
  if (inX && pos == curX.fromB && !enteredX) {
290
248
  deleted = Span.join(deleted, curX.deleted, combine);
291
249
  toA += curX.lenA;
292
250
  enteredX = true;
293
251
  }
294
-
295
252
  if (inX && !inY) {
296
253
  inserted = Span.join(inserted, Span.slice(curX.inserted, pos - curX.fromB, next - curX.fromB), combine);
297
254
  toB += next - pos;
298
255
  }
299
-
300
256
  if (inY && pos == curY.fromA && !enteredY) {
301
257
  inserted = Span.join(inserted, curY.inserted, combine);
302
258
  toB += curY.lenB;
303
259
  enteredY = true;
304
260
  }
305
-
306
261
  if (inY && !inX) {
307
262
  deleted = Span.join(deleted, Span.slice(curY.deleted, pos - curY.fromA, next - curY.fromA), combine);
308
263
  toA += next - pos;
309
264
  }
310
-
311
265
  if (inX && next == curX.toB) {
312
266
  curX = iX++ == x.length ? null : x[iX];
313
267
  enteredX = false;
314
268
  }
315
-
316
269
  if (inY && next == curY.toA) {
317
270
  curY = iY++ == y.length ? null : y[iY];
318
271
  enteredY = false;
319
272
  }
320
-
321
273
  pos = next;
322
274
  }
323
-
324
275
  if (fromA < toA || fromB < toB) result.push(new Change(fromA, toA, fromB, toB, deleted, inserted));
325
276
  }
326
277
  }
327
278
  }
328
279
  }]);
329
-
330
280
  return Change;
331
281
  }();
332
-
333
282
  var letter;
334
-
335
283
  try {
336
284
  letter = new RegExp("[\\p{Alphabetic}_]", "u");
337
285
  } catch (_) {}
338
-
339
286
  var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
340
-
341
287
  function isLetter(code) {
342
288
  if (code < 128) return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 79 && code <= 122;
343
289
  var ch = String.fromCharCode(code);
344
290
  if (letter) return letter.test(ch);
345
291
  return ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch);
346
292
  }
347
-
348
293
  function getText(frag, start, end) {
349
294
  var out = "";
350
-
351
295
  function convert(frag, start, end) {
352
296
  for (var i = 0, off = 0; i < frag.childCount; i++) {
353
297
  var child = frag.child(i),
354
- endOff = off + child.nodeSize;
298
+ endOff = off + child.nodeSize;
355
299
  var from = Math.max(off, start),
356
- to = Math.min(endOff, end);
357
-
300
+ to = Math.min(endOff, end);
358
301
  if (from < to) {
359
302
  if (child.isText) {
360
303
  out += child.text.slice(Math.max(0, start - off), Math.min(child.text.length, end - off));
@@ -366,104 +309,76 @@ function getText(frag, start, end) {
366
309
  if (to == endOff) out += " ";
367
310
  }
368
311
  }
369
-
370
312
  off = endOff;
371
313
  }
372
314
  }
373
-
374
315
  convert(frag, start, end);
375
316
  return out;
376
317
  }
377
-
378
318
  var MAX_SIMPLIFY_DISTANCE = 30;
379
-
380
319
  function simplifyChanges(changes, doc) {
381
320
  var result = [];
382
-
383
321
  for (var i = 0; i < changes.length; i++) {
384
322
  var end = changes[i].toB,
385
- start = i;
386
-
387
- while (i < changes.length - 1 && changes[i + 1].fromB <= end + MAX_SIMPLIFY_DISTANCE) {
388
- end = changes[++i].toB;
389
- }
390
-
323
+ start = i;
324
+ while (i < changes.length - 1 && changes[i + 1].fromB <= end + MAX_SIMPLIFY_DISTANCE) end = changes[++i].toB;
391
325
  simplifyAdjacentChanges(changes, start, i + 1, doc, result);
392
326
  }
393
-
394
327
  return result;
395
328
  }
396
-
397
329
  function simplifyAdjacentChanges(changes, from, to, doc, target) {
398
330
  var start = Math.max(0, changes[from].fromB - MAX_SIMPLIFY_DISTANCE);
399
331
  var end = Math.min(doc.content.size, changes[to - 1].toB + MAX_SIMPLIFY_DISTANCE);
400
332
  var text = getText(doc.content, start, end);
401
-
402
333
  for (var i = from; i < to; i++) {
403
334
  var startI = i,
404
- last = changes[i],
405
- deleted = last.lenA,
406
- inserted = last.lenB;
407
-
335
+ last = changes[i],
336
+ deleted = last.lenA,
337
+ inserted = last.lenB;
408
338
  while (i < to - 1) {
409
339
  var next = changes[i + 1],
410
- boundary = false;
340
+ boundary = false;
411
341
  var prevLetter = last.toB == end ? false : isLetter(text.charCodeAt(last.toB - 1 - start));
412
-
413
342
  for (var pos = last.toB; !boundary && pos < next.fromB; pos++) {
414
343
  var nextLetter = pos == end ? false : isLetter(text.charCodeAt(pos - start));
415
344
  if ((!prevLetter || !nextLetter) && pos != changes[startI].fromB) boundary = true;
416
345
  prevLetter = nextLetter;
417
346
  }
418
-
419
347
  if (boundary) break;
420
348
  deleted += next.lenA;
421
349
  inserted += next.lenB;
422
350
  last = next;
423
351
  i++;
424
352
  }
425
-
426
353
  if (inserted > 0 && deleted > 0 && !(inserted == 1 && deleted == 1)) {
427
354
  var _from = changes[startI].fromB,
428
- _to = changes[i].toB;
429
- if (_from < end && isLetter(text.charCodeAt(_from - start))) while (_from > start && isLetter(text.charCodeAt(_from - 1 - start))) {
430
- _from--;
431
- }
432
- if (_to > start && isLetter(text.charCodeAt(_to - 1 - start))) while (_to < end && isLetter(text.charCodeAt(_to - start))) {
433
- _to++;
434
- }
355
+ _to = changes[i].toB;
356
+ if (_from < end && isLetter(text.charCodeAt(_from - start))) while (_from > start && isLetter(text.charCodeAt(_from - 1 - start))) _from--;
357
+ if (_to > start && isLetter(text.charCodeAt(_to - 1 - start))) while (_to < end && isLetter(text.charCodeAt(_to - start))) _to++;
435
358
  var joined = fillChange(changes.slice(startI, i + 1), _from, _to);
436
-
437
359
  var _last = target.length ? target[target.length - 1] : null;
438
-
439
360
  if (_last && _last.toA == joined.fromA) target[target.length - 1] = new Change(_last.fromA, joined.toA, _last.fromB, joined.toB, _last.deleted.concat(joined.deleted), _last.inserted.concat(joined.inserted));else target.push(joined);
440
361
  } else {
441
- for (var j = startI; j <= i; j++) {
442
- target.push(changes[j]);
443
- }
362
+ for (var j = startI; j <= i; j++) target.push(changes[j]);
444
363
  }
445
364
  }
446
-
447
365
  return changes;
448
366
  }
449
-
450
367
  function combine(a, b) {
451
368
  return a === b ? a : null;
452
369
  }
453
-
454
370
  function fillChange(changes, fromB, toB) {
455
371
  var fromA = changes[0].fromA - (changes[0].fromB - fromB);
456
372
  var last = changes[changes.length - 1];
457
373
  var toA = last.toA + (toB - last.toB);
458
374
  var deleted = Span.none,
459
- inserted = Span.none;
375
+ inserted = Span.none;
460
376
  var delData = (changes[0].deleted.length ? changes[0].deleted : changes[0].inserted)[0].data;
461
377
  var insData = (changes[0].inserted.length ? changes[0].inserted : changes[0].deleted)[0].data;
462
-
463
378
  for (var posA = fromA, posB = fromB, i = 0;; i++) {
464
379
  var next = i == changes.length ? null : changes[i];
465
380
  var endA = next ? next.fromA : toA,
466
- endB = next ? next.fromB : toB;
381
+ endB = next ? next.fromB : toB;
467
382
  if (endA > posA) deleted = Span.join(deleted, [new Span(endA - posA, delData)], combine);
468
383
  if (endB > posB) inserted = Span.join(inserted, [new Span(endB - posB, insData)], combine);
469
384
  if (!next) break;
@@ -474,26 +389,20 @@ function fillChange(changes, fromB, toB) {
474
389
  posA = next.toA;
475
390
  posB = next.toB;
476
391
  }
477
-
478
392
  return new Change(fromA, toA, fromB, toB, deleted, inserted);
479
393
  }
480
-
481
394
  var ChangeSet = function () {
482
395
  function ChangeSet(config, changes) {
483
396
  _classCallCheck(this, ChangeSet);
484
-
485
397
  this.config = config;
486
398
  this.changes = changes;
487
399
  }
488
-
489
400
  _createClass(ChangeSet, [{
490
401
  key: "addSteps",
491
402
  value: function addSteps(newDoc, maps, data) {
492
403
  var _this = this;
493
-
494
404
  var stepChanges = [];
495
-
496
- var _loop = function _loop(i) {
405
+ var _loop2 = function _loop2() {
497
406
  var d = Array.isArray(data) ? data[i] : data;
498
407
  var off = 0;
499
408
  maps[i].forEach(function (fromA, toA, fromB, toB) {
@@ -501,54 +410,41 @@ var ChangeSet = function () {
501
410
  off = toB - fromB - (toA - fromA);
502
411
  });
503
412
  };
504
-
505
413
  for (var i = 0; i < maps.length; i++) {
506
- _loop(i);
414
+ _loop2();
507
415
  }
508
-
509
416
  if (stepChanges.length == 0) return this;
510
417
  var newChanges = mergeAll(stepChanges, this.config.combine);
511
418
  var changes = Change.merge(this.changes, newChanges, this.config.combine);
512
419
  var updated = changes;
513
-
514
- var _loop2 = function _loop2(_i3) {
515
- var change = updated[_i3];
516
-
517
- if (change.fromA == change.toA || change.fromB == change.toB || !newChanges.some(function (r) {
518
- return r.toB > change.fromB && r.fromB < change.toB;
519
- })) {
520
- _i2 = _i3;
521
- return "continue";
522
- }
523
-
524
- var diff = computeDiff(_this.config.doc.content, newDoc.content, change);
525
-
526
- if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB) {
420
+ var _loop3 = function _loop3(_i3) {
421
+ var change = updated[_i3];
422
+ if (change.fromA == change.toA || change.fromB == change.toB || !newChanges.some(function (r) {
423
+ return r.toB > change.fromB && r.fromB < change.toB;
424
+ })) {
425
+ _i2 = _i3;
426
+ return 0;
427
+ }
428
+ var diff = computeDiff(_this.config.doc.content, newDoc.content, change, _this.config.encoder);
429
+ if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB) {
430
+ _i2 = _i3;
431
+ return 0;
432
+ }
433
+ if (updated == changes) updated = changes.slice();
434
+ if (diff.length == 1) {
435
+ updated[_i3] = diff[0];
436
+ } else {
437
+ var _updated;
438
+ (_updated = updated).splice.apply(_updated, [_i3, 1].concat(_toConsumableArray(diff)));
439
+ _i3 += diff.length - 1;
440
+ }
527
441
  _i2 = _i3;
528
- return "continue";
529
- }
530
-
531
- if (updated == changes) updated = changes.slice();
532
-
533
- if (diff.length == 1) {
534
- updated[_i3] = diff[0];
535
- } else {
536
- var _updated;
537
-
538
- (_updated = updated).splice.apply(_updated, [_i3, 1].concat(_toConsumableArray(diff)));
539
-
540
- _i3 += diff.length - 1;
541
- }
542
-
543
- _i2 = _i3;
544
- };
545
-
442
+ },
443
+ _ret2;
546
444
  for (var _i2 = 0; _i2 < updated.length; _i2++) {
547
- var _ret2 = _loop2(_i2);
548
-
549
- if (_ret2 === "continue") continue;
445
+ _ret2 = _loop3(_i2);
446
+ if (_ret2 === 0) continue;
550
447
  }
551
-
552
448
  return new ChangeSet(this.config, updated);
553
449
  }
554
450
  }, {
@@ -563,7 +459,6 @@ var ChangeSet = function () {
563
459
  var newData = f(span);
564
460
  return newData === span.data ? span : new Span(span.length, newData);
565
461
  };
566
-
567
462
  return new ChangeSet(this.config, this.changes.map(function (ch) {
568
463
  return new Change(ch.fromA, ch.toA, ch.fromB, ch.toB, ch.deleted.map(mapSpan), ch.inserted.map(mapSpan));
569
464
  }));
@@ -574,27 +469,21 @@ var ChangeSet = function () {
574
469
  if (b == this) return null;
575
470
  var touched = maps && touchedRange(maps);
576
471
  var moved = touched ? touched.toB - touched.fromB - (touched.toA - touched.fromA) : 0;
577
-
578
472
  function map(p) {
579
473
  return !touched || p <= touched.fromA ? p : p + moved;
580
474
  }
581
-
582
475
  var from = touched ? touched.fromB : 2e8,
583
- to = touched ? touched.toB : -2e8;
584
-
476
+ to = touched ? touched.toB : -2e8;
585
477
  function add(start) {
586
478
  var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : start;
587
479
  from = Math.min(start, from);
588
480
  to = Math.max(end, to);
589
481
  }
590
-
591
482
  var rA = this.changes,
592
- rB = b.changes;
593
-
483
+ rB = b.changes;
594
484
  for (var iA = 0, iB = 0; iA < rA.length && iB < rB.length;) {
595
485
  var rangeA = rA[iA],
596
- rangeB = rB[iB];
597
-
486
+ rangeB = rB[iB];
598
487
  if (rangeA && rangeB && sameRanges(rangeA, rangeB, map)) {
599
488
  iA++;
600
489
  iB++;
@@ -606,7 +495,6 @@ var ChangeSet = function () {
606
495
  iA++;
607
496
  }
608
497
  }
609
-
610
498
  return from <= to ? {
611
499
  from: from,
612
500
  to: to
@@ -618,18 +506,17 @@ var ChangeSet = function () {
618
506
  var combine = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (a, b) {
619
507
  return a === b ? a : null;
620
508
  };
509
+ var tokenEncoder = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DefaultEncoder;
621
510
  return new ChangeSet({
622
511
  combine: combine,
623
- doc: doc
512
+ doc: doc,
513
+ encoder: tokenEncoder
624
514
  }, []);
625
515
  }
626
516
  }]);
627
-
628
517
  return ChangeSet;
629
518
  }();
630
-
631
519
  ChangeSet.computeDiff = computeDiff;
632
-
633
520
  function mergeAll(ranges, combine) {
634
521
  var start = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
635
522
  var end = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ranges.length;
@@ -637,31 +524,25 @@ function mergeAll(ranges, combine) {
637
524
  var mid = start + end >> 1;
638
525
  return Change.merge(mergeAll(ranges, combine, start, mid), mergeAll(ranges, combine, mid, end), combine);
639
526
  }
640
-
641
527
  function endRange(maps) {
642
528
  var from = 2e8,
643
- to = -2e8;
644
-
529
+ to = -2e8;
645
530
  for (var i = 0; i < maps.length; i++) {
646
531
  var map = maps[i];
647
-
648
532
  if (from != 2e8) {
649
533
  from = map.map(from, -1);
650
534
  to = map.map(to, 1);
651
535
  }
652
-
653
536
  map.forEach(function (_s, _e, start, end) {
654
537
  from = Math.min(from, start);
655
538
  to = Math.max(to, end);
656
539
  });
657
540
  }
658
-
659
541
  return from == 2e8 ? null : {
660
542
  from: from,
661
543
  to: to
662
544
  };
663
545
  }
664
-
665
546
  function touchedRange(maps) {
666
547
  var b = endRange(maps);
667
548
  if (!b) return null;
@@ -675,21 +556,14 @@ function touchedRange(maps) {
675
556
  toB: b.to
676
557
  };
677
558
  }
678
-
679
559
  function sameRanges(a, b, map) {
680
560
  return map(a.fromB) == b.fromB && map(a.toB) == b.toB && sameSpans(a.deleted, b.deleted) && sameSpans(a.inserted, b.inserted);
681
561
  }
682
-
683
562
  function sameSpans(a, b) {
684
563
  if (a.length != b.length) return false;
685
-
686
- for (var i = 0; i < a.length; i++) {
687
- if (a[i].length != b[i].length || a[i].data !== b[i].data) return false;
688
- }
689
-
564
+ for (var i = 0; i < a.length; i++) if (a[i].length != b[i].length || a[i].data !== b[i].data) return false;
690
565
  return true;
691
566
  }
692
-
693
567
  exports.Change = Change;
694
568
  exports.ChangeSet = ChangeSet;
695
569
  exports.Span = Span;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Node } from 'prosemirror-model';
1
+ import { Mark, Node } from 'prosemirror-model';
2
2
  import { StepMap } from 'prosemirror-transform';
3
3
 
4
4
  /**
@@ -41,7 +41,7 @@ declare class Change<Data = any> {
41
41
  readonly deleted: readonly Span<Data>[];
42
42
  /**
43
43
  Data associated with the inserted content. Length adds up to
44
- `this.toB - this.toA`.
44
+ `this.toB - this.fromB`.
45
45
  */
46
46
  readonly inserted: readonly Span<Data>[];
47
47
  /**
@@ -52,6 +52,38 @@ declare class Change<Data = any> {
52
52
  static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
53
  }
54
54
 
55
+ /**
56
+ A token encoder can be passed when creating a `ChangeSet` in order
57
+ to influence the way the library runs its diffing algorithm. The
58
+ encoder determines how document tokens (such as nodes and
59
+ characters) are encoded and compared.
60
+
61
+ Note that both the encoding and the comparison may run a lot, and
62
+ doing non-trivial work in these functions could impact
63
+ performance.
64
+ */
65
+ interface TokenEncoder<T> {
66
+ /**
67
+ Encode a given character, with the given marks applied.
68
+ */
69
+ encodeCharacter(char: number, marks: readonly Mark[]): T;
70
+ /**
71
+ Encode the start of a node or, if this is a leaf node, the
72
+ entire node.
73
+ */
74
+ encodeNodeStart(node: Node): T;
75
+ /**
76
+ Encode the end token for the given node. It is valid to encode
77
+ every end token in the same way.
78
+ */
79
+ encodeNodeEnd(node: Node): T;
80
+ /**
81
+ Compare the given tokens. Should return true when they count as
82
+ equal.
83
+ */
84
+ compareTokens(a: T, b: T): boolean;
85
+ }
86
+
55
87
  /**
56
88
  Simplifies a set of changes for presentation. This makes the
57
89
  assumption that having both insertions and deletions within a word
@@ -108,11 +140,17 @@ declare class ChangeSet<Data = any> {
108
140
  } | null;
109
141
  /**
110
142
  Create a changeset with the given base object and configuration.
143
+
111
144
  The `combine` function is used to compare and combine metadata—it
112
145
  should return null when metadata isn't compatible, and a combined
113
146
  version for a merged range when it is.
147
+
148
+ When given, a token encoder determines how document tokens are
149
+ serialized and compared when diffing the content produced by
150
+ changes. The default is to just compare nodes by name and text
151
+ by character, ignoring marks and attributes.
114
152
  */
115
- static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data): ChangeSet<Data>;
153
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
116
154
  }
117
155
 
118
- export { Change, ChangeSet, Span, simplifyChanges };
156
+ export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Node } from 'prosemirror-model';
1
+ import { Mark, Node } from 'prosemirror-model';
2
2
  import { StepMap } from 'prosemirror-transform';
3
3
 
4
4
  /**
@@ -41,7 +41,7 @@ declare class Change<Data = any> {
41
41
  readonly deleted: readonly Span<Data>[];
42
42
  /**
43
43
  Data associated with the inserted content. Length adds up to
44
- `this.toB - this.toA`.
44
+ `this.toB - this.fromB`.
45
45
  */
46
46
  readonly inserted: readonly Span<Data>[];
47
47
  /**
@@ -52,6 +52,38 @@ declare class Change<Data = any> {
52
52
  static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
53
  }
54
54
 
55
+ /**
56
+ A token encoder can be passed when creating a `ChangeSet` in order
57
+ to influence the way the library runs its diffing algorithm. The
58
+ encoder determines how document tokens (such as nodes and
59
+ characters) are encoded and compared.
60
+
61
+ Note that both the encoding and the comparison may run a lot, and
62
+ doing non-trivial work in these functions could impact
63
+ performance.
64
+ */
65
+ interface TokenEncoder<T> {
66
+ /**
67
+ Encode a given character, with the given marks applied.
68
+ */
69
+ encodeCharacter(char: number, marks: readonly Mark[]): T;
70
+ /**
71
+ Encode the start of a node or, if this is a leaf node, the
72
+ entire node.
73
+ */
74
+ encodeNodeStart(node: Node): T;
75
+ /**
76
+ Encode the end token for the given node. It is valid to encode
77
+ every end token in the same way.
78
+ */
79
+ encodeNodeEnd(node: Node): T;
80
+ /**
81
+ Compare the given tokens. Should return true when they count as
82
+ equal.
83
+ */
84
+ compareTokens(a: T, b: T): boolean;
85
+ }
86
+
55
87
  /**
56
88
  Simplifies a set of changes for presentation. This makes the
57
89
  assumption that having both insertions and deletions within a word
@@ -108,11 +140,17 @@ declare class ChangeSet<Data = any> {
108
140
  } | null;
109
141
  /**
110
142
  Create a changeset with the given base object and configuration.
143
+
111
144
  The `combine` function is used to compare and combine metadata—it
112
145
  should return null when metadata isn't compatible, and a combined
113
146
  version for a merged range when it is.
147
+
148
+ When given, a token encoder determines how document tokens are
149
+ serialized and compared when diffing the content produced by
150
+ changes. The default is to just compare nodes by name and text
151
+ by character, ignoring marks and attributes.
114
152
  */
115
- static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data): ChangeSet<Data>;
153
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
116
154
  }
117
155
 
118
- export { Change, ChangeSet, Span, simplifyChanges };
156
+ export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.js CHANGED
@@ -1,24 +1,38 @@
1
- // Convert the given range of a fragment to tokens, where node open
2
- // tokens are encoded as strings holding the node name, characters as
3
- // their character code, and node close tokens as -1.
4
- function tokens(frag, start, end, target) {
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.
11
+ const DefaultEncoder = {
12
+ encodeCharacter: char => char,
13
+ encodeNodeStart: node => node.type.name,
14
+ encodeNodeEnd: node => -typeID(node.type),
15
+ compareTokens: (a, b) => a === b
16
+ };
17
+ // Convert the given range of a fragment to tokens.
18
+ function tokens(frag, encoder, start, end, target) {
5
19
  for (let i = 0, off = 0; i < frag.childCount; i++) {
6
20
  let child = frag.child(i), endOff = off + child.nodeSize;
7
21
  let from = Math.max(off, start), to = Math.min(endOff, end);
8
22
  if (from < to) {
9
23
  if (child.isText) {
10
24
  for (let j = from; j < to; j++)
11
- target.push(child.text.charCodeAt(j - off));
25
+ target.push(encoder.encodeCharacter(child.text.charCodeAt(j - off), child.marks));
12
26
  }
13
27
  else if (child.isLeaf) {
14
- target.push(child.type.name);
28
+ target.push(encoder.encodeNodeStart(child));
15
29
  }
16
30
  else {
17
31
  if (from == off)
18
- target.push(child.type.name);
19
- tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
32
+ target.push(encoder.encodeNodeStart(child));
33
+ tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
20
34
  if (to == endOff)
21
- target.push(-1);
35
+ target.push(encoder.encodeNodeEnd(child));
22
36
  }
23
37
  }
24
38
  off = endOff;
@@ -37,16 +51,17 @@ const MAX_DIFF_SIZE = 5000;
37
51
  function minUnchanged(sizeA, sizeB) {
38
52
  return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
39
53
  }
40
- function computeDiff(fragA, fragB, range) {
41
- let tokA = tokens(fragA, range.fromA, range.toA, []);
42
- let tokB = tokens(fragB, range.fromB, range.toB, []);
54
+ function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
55
+ let tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
56
+ let tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
43
57
  // Scan from both sides to cheaply eliminate work
44
58
  let start = 0, endA = tokA.length, endB = tokB.length;
45
- while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start])
59
+ let cmp = encoder.compareTokens;
60
+ while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start]))
46
61
  start++;
47
62
  if (start == tokA.length && start == tokB.length)
48
63
  return [];
49
- while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1])
64
+ while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1]))
50
65
  endA--, endB--;
51
66
  // If the result is simple _or_ too big to cheaply compute, return
52
67
  // the remaining region as the diff
@@ -65,7 +80,7 @@ function computeDiff(fragA, fragB, range) {
65
80
  for (let diag = -size; diag <= size; diag += 2) {
66
81
  let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max];
67
82
  let x = next < prev ? prev : next + 1, y = x + diag;
68
- while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y])
83
+ while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y]))
69
84
  x++, y++;
70
85
  frontier[diag + max] = x;
71
86
  // Found a match
@@ -225,7 +240,7 @@ class Change {
225
240
  deleted,
226
241
  /**
227
242
  Data associated with the inserted content. Length adds up to
228
- `this.toB - this.toA`.
243
+ `this.toB - this.fromB`.
229
244
  */
230
245
  inserted) {
231
246
  this.fromA = fromA;
@@ -549,7 +564,7 @@ class ChangeSet {
549
564
  // Only look at changes that touch newly added changed ranges
550
565
  !newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB))
551
566
  continue;
552
- let diff = computeDiff(this.config.doc.content, newDoc.content, change);
567
+ let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder);
553
568
  // Fast path: If they are completely different, don't do anything
554
569
  if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)
555
570
  continue;
@@ -622,12 +637,18 @@ class ChangeSet {
622
637
  }
623
638
  /**
624
639
  Create a changeset with the given base object and configuration.
640
+
625
641
  The `combine` function is used to compare and combine metadata—it
626
642
  should return null when metadata isn't compatible, and a combined
627
643
  version for a merged range when it is.
644
+
645
+ When given, a token encoder determines how document tokens are
646
+ serialized and compared when diffing the content produced by
647
+ changes. The default is to just compare nodes by name and text
648
+ by character, ignoring marks and attributes.
628
649
  */
629
- static create(doc, combine = (a, b) => a === b ? a : null) {
630
- return new ChangeSet({ combine, doc }, []);
650
+ static create(doc, combine = (a, b) => a === b ? a : null, tokenEncoder = DefaultEncoder) {
651
+ return new ChangeSet({ combine, doc, encoder: tokenEncoder }, []);
631
652
  }
632
653
  }
633
654
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-changeset",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
4
4
  "description": "Distills a series of editing steps into deleted and added ranges",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -29,11 +29,12 @@
29
29
  "devDependencies": {
30
30
  "@prosemirror/buildhelper": "^0.1.5",
31
31
  "prosemirror-model": "^1.0.0",
32
- "prosemirror-test-builder": "^1.0.0"
32
+ "prosemirror-test-builder": "^1.0.0",
33
+ "builddocs": "^1.0.8"
33
34
  },
34
35
  "scripts": {
35
36
  "test": "pm-runtests",
36
37
  "prepare": "pm-buildhelper src/changeset.ts",
37
- "build-readme": "FIXME"
38
+ "build-readme": "builddocs --format markdown --main src/README.md src/changeset.ts > README.md"
38
39
  }
39
40
  }
package/src/README.md CHANGED
@@ -25,4 +25,6 @@ it was made, or the step data necessary to invert it again.
25
25
 
26
26
  @ChangeSet
27
27
 
28
- @simplifyChanges
28
+ @simplifyChanges
29
+
30
+ @TokenEncoder
package/src/change.ts CHANGED
@@ -66,7 +66,7 @@ export class Change<Data = any> {
66
66
  /// spans adds up to `this.toA - this.fromA`.
67
67
  readonly deleted: readonly Span<Data>[],
68
68
  /// Data associated with the inserted content. Length adds up to
69
- /// `this.toB - this.toA`.
69
+ /// `this.toB - this.fromB`.
70
70
  readonly inserted: readonly Span<Data>[]
71
71
  ) {}
72
72
 
package/src/changeset.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import {Node} from "prosemirror-model"
2
2
  import {StepMap} from "prosemirror-transform"
3
- import {computeDiff} from "./diff"
3
+ import {computeDiff, TokenEncoder, DefaultEncoder} from "./diff"
4
4
  import {Change, Span} from "./change"
5
5
  export {Change, Span}
6
6
  export {simplifyChanges} from "./simplify"
7
+ export {TokenEncoder}
7
8
 
8
9
  /// A change set tracks the changes to a document from a given point
9
10
  /// in the past. It condenses a number of step maps down to a flat
@@ -13,7 +14,11 @@ export class ChangeSet<Data = any> {
13
14
  /// @internal
14
15
  constructor(
15
16
  /// @internal
16
- readonly config: {doc: Node, combine: (dataA: Data, dataB: Data) => Data},
17
+ readonly config: {
18
+ doc: Node,
19
+ combine: (dataA: Data, dataB: Data) => Data,
20
+ encoder: TokenEncoder<any>
21
+ },
17
22
  /// Replaced regions.
18
23
  readonly changes: readonly Change<Data>[]
19
24
  ) {}
@@ -69,7 +74,7 @@ export class ChangeSet<Data = any> {
69
74
  if (change.fromA == change.toA || change.fromB == change.toB ||
70
75
  // Only look at changes that touch newly added changed ranges
71
76
  !newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB)) continue
72
- let diff = computeDiff(this.config.doc.content, newDoc.content, change)
77
+ let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder)
73
78
 
74
79
  // Fast path: If they are completely different, don't do anything
75
80
  if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)
@@ -132,11 +137,21 @@ export class ChangeSet<Data = any> {
132
137
  }
133
138
 
134
139
  /// Create a changeset with the given base object and configuration.
140
+ ///
135
141
  /// The `combine` function is used to compare and combine metadata—it
136
142
  /// should return null when metadata isn't compatible, and a combined
137
143
  /// version for a merged range when it is.
138
- static create<Data = any>(doc: Node, combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any) {
139
- return new ChangeSet({combine, doc}, [])
144
+ ///
145
+ /// When given, a token encoder determines how document tokens are
146
+ /// serialized and compared when diffing the content produced by
147
+ /// changes. The default is to just compare nodes by name and text
148
+ /// by character, ignoring marks and attributes.
149
+ static create<Data = any>(
150
+ doc: Node,
151
+ combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any,
152
+ tokenEncoder: TokenEncoder<any> = DefaultEncoder
153
+ ) {
154
+ return new ChangeSet({combine, doc, encoder: tokenEncoder}, [])
140
155
  }
141
156
 
142
157
  /// Exported for testing @internal
package/src/diff.ts CHANGED
@@ -1,22 +1,59 @@
1
- import {Fragment} from "prosemirror-model"
1
+ import {Fragment, Node, NodeType, Mark} from "prosemirror-model"
2
2
  import {Change} from "./change"
3
3
 
4
- // Convert the given range of a fragment to tokens, where node open
5
- // tokens are encoded as strings holding the node name, characters as
6
- // their character code, and node close tokens as -1.
7
- function tokens(frag: Fragment, start: number, end: number, target: (number | string)[]) {
4
+ /// A token encoder can be passed when creating a `ChangeSet` in order
5
+ /// to influence the way the library runs its diffing algorithm. The
6
+ /// encoder determines how document tokens (such as nodes and
7
+ /// characters) are encoded and compared.
8
+ ///
9
+ /// Note that both the encoding and the comparison may run a lot, and
10
+ /// doing non-trivial work in these functions could impact
11
+ /// performance.
12
+ export interface TokenEncoder<T> {
13
+ /// Encode a given character, with the given marks applied.
14
+ encodeCharacter(char: number, marks: readonly Mark[]): T
15
+ /// Encode the start of a node or, if this is a leaf node, the
16
+ /// entire node.
17
+ encodeNodeStart(node: Node): T
18
+ /// Encode the end token for the given node. It is valid to encode
19
+ /// every end token in the same way.
20
+ encodeNodeEnd(node: Node): T
21
+ /// Compare the given tokens. Should return true when they count as
22
+ /// equal.
23
+ compareTokens(a: T, b: T): boolean
24
+ }
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.
36
+ export const DefaultEncoder: TokenEncoder<number | string> = {
37
+ encodeCharacter: char => char,
38
+ encodeNodeStart: node => node.type.name,
39
+ encodeNodeEnd: node => -typeID(node.type),
40
+ compareTokens: (a, b) => a === b
41
+ }
42
+
43
+ // Convert the given range of a fragment to tokens.
44
+ function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end: number, target: T[]) {
8
45
  for (let i = 0, off = 0; i < frag.childCount; i++) {
9
46
  let child = frag.child(i), endOff = off + child.nodeSize
10
47
  let from = Math.max(off, start), to = Math.min(endOff, end)
11
48
  if (from < to) {
12
49
  if (child.isText) {
13
- for (let j = from; j < to; j++) target.push(child.text!.charCodeAt(j - off))
50
+ for (let j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text!.charCodeAt(j - off), child.marks))
14
51
  } else if (child.isLeaf) {
15
- target.push(child.type.name)
52
+ target.push(encoder.encodeNodeStart(child))
16
53
  } else {
17
- if (from == off) target.push(child.type.name)
18
- tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
19
- if (to == endOff) target.push(-1)
54
+ if (from == off) target.push(encoder.encodeNodeStart(child))
55
+ tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
56
+ if (to == endOff) target.push(encoder.encodeNodeEnd(child))
20
57
  }
21
58
  }
22
59
  off = endOff
@@ -38,15 +75,16 @@ function minUnchanged(sizeA: number, sizeB: number) {
38
75
  return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)))
39
76
  }
40
77
 
41
- export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change) {
42
- let tokA = tokens(fragA, range.fromA, range.toA, [])
43
- let tokB = tokens(fragB, range.fromB, range.toB, [])
78
+ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, encoder: TokenEncoder<any> = DefaultEncoder) {
79
+ let tokA = tokens(fragA, encoder, range.fromA, range.toA, [])
80
+ let tokB = tokens(fragB, encoder, range.fromB, range.toB, [])
44
81
 
45
82
  // Scan from both sides to cheaply eliminate work
46
83
  let start = 0, endA = tokA.length, endB = tokB.length
47
- while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start]) start++
84
+ let cmp = encoder.compareTokens
85
+ while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++
48
86
  if (start == tokA.length && start == tokB.length) return []
49
- while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1]) endA--, endB--
87
+ while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--
50
88
  // If the result is simple _or_ too big to cheaply compute, return
51
89
  // the remaining region as the diff
52
90
  if (endA == start || endB == start || (endA == endB && endA == start + 1))
@@ -66,7 +104,7 @@ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change) {
66
104
  for (let diag = -size; diag <= size; diag += 2) {
67
105
  let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max]
68
106
  let x = next < prev ? prev : next + 1, y = x + diag
69
- while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y]) x++, y++
107
+ while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++
70
108
  frontier[diag + max] = x
71
109
  // Found a match
72
110
  if (x >= lenA && y >= lenB) {
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
  })