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