prosemirror-changeset 2.4.0 → 2.4.2
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 +12 -0
- package/README.md +1 -1
- package/dist/index.cjs +25 -24
- package/dist/index.js +12 -8
- package/package.json +2 -2
- package/src/README.md +1 -1
- package/src/diff.ts +13 -8
- package/src/simplify.ts +1 -1
- package/test/test-simplify.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 2.4.2 (2026-08-21)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Fix an issue where the diffing of changed ranges would waste a bunch of time building up tokens and trying to compute a diff even if the ranges were too big to find a solution for.
|
|
6
|
+
|
|
7
|
+
## 2.4.1 (2026-04-14)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
Fix a bug that would cause change simplification to sometimes expand changes over non-word characters.
|
|
12
|
+
|
|
1
13
|
## 2.4.0 (2026-02-14)
|
|
2
14
|
|
|
3
15
|
### New features
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ in order to do such change tracking in a halfway performant way during
|
|
|
7
7
|
live editing.
|
|
8
8
|
|
|
9
9
|
This code is licensed under an [MIT
|
|
10
|
-
licence](https://
|
|
10
|
+
licence](https://code.haverbeke.berlin/prosemirror/prosemirror-changeset/src/branch/main/LICENSE).
|
|
11
11
|
|
|
12
12
|
## Programming interface
|
|
13
13
|
|
package/dist/index.cjs
CHANGED
|
@@ -53,29 +53,30 @@ function tokens(frag, encoder, start, end, target) {
|
|
|
53
53
|
}
|
|
54
54
|
return target;
|
|
55
55
|
}
|
|
56
|
-
var MAX_DIFF_SIZE =
|
|
56
|
+
var MAX_DIFF_SIZE = 2500;
|
|
57
57
|
function minUnchanged(sizeA, sizeB) {
|
|
58
58
|
return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
|
|
59
59
|
}
|
|
60
60
|
function computeDiff(fragA, fragB, range) {
|
|
61
61
|
var encoder = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DefaultEncoder;
|
|
62
|
+
if (Math.max(range.toA - range.fromA, range.toB, range.fromB) > MAX_DIFF_SIZE) return [range];
|
|
62
63
|
var tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
|
|
63
64
|
var tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
|
|
65
|
+
return diff(tokA, tokB, range, encoder.compareTokens);
|
|
66
|
+
}
|
|
67
|
+
function diff(tokA, tokB, range, cmp) {
|
|
64
68
|
var start = 0,
|
|
65
69
|
endA = tokA.length,
|
|
66
70
|
endB = tokB.length;
|
|
67
|
-
var cmp = encoder.compareTokens;
|
|
68
71
|
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++;
|
|
69
72
|
if (start == tokA.length && start == tokB.length) return [];
|
|
70
73
|
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--;
|
|
71
74
|
if (endA == start || endB == start || endA == endB && endA == start + 1) return [range.slice(start, endA, start, endB)];
|
|
72
75
|
var lenA = endA - start,
|
|
73
76
|
lenB = endB - start;
|
|
74
|
-
var max = Math.min(MAX_DIFF_SIZE, lenA + lenB)
|
|
75
|
-
off = max + 1;
|
|
77
|
+
var max = Math.min(MAX_DIFF_SIZE, lenA + lenB);
|
|
76
78
|
var history = [];
|
|
77
|
-
var frontier =
|
|
78
|
-
for (var len = off * 2, i = 0; i < len; i++) frontier[i] = -1;
|
|
79
|
+
var frontier = new Int16Array((max + 1) * 2).fill(-1);
|
|
79
80
|
for (var size = 0; size <= max; size++) {
|
|
80
81
|
var _loop = function _loop(_diag) {
|
|
81
82
|
var next = frontier[_diag + 1 + max],
|
|
@@ -85,7 +86,7 @@ function computeDiff(fragA, fragB, range) {
|
|
|
85
86
|
while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++;
|
|
86
87
|
frontier[_diag + max] = x;
|
|
87
88
|
if (x >= lenA && y >= lenB) {
|
|
88
|
-
var
|
|
89
|
+
var _diff = [],
|
|
89
90
|
minSpan = minUnchanged(endA - start, endB - start);
|
|
90
91
|
var fromA = -1,
|
|
91
92
|
toA = -1,
|
|
@@ -96,14 +97,14 @@ function computeDiff(fragA, fragB, range) {
|
|
|
96
97
|
fromA = fA;
|
|
97
98
|
fromB = fB;
|
|
98
99
|
} else {
|
|
99
|
-
if (fromA > -1)
|
|
100
|
+
if (fromA > -1) _diff.push(range.slice(fromA, toA, fromB, toB));
|
|
100
101
|
fromA = fA;
|
|
101
102
|
toA = tA;
|
|
102
103
|
fromB = fB;
|
|
103
104
|
toB = tB;
|
|
104
105
|
}
|
|
105
106
|
};
|
|
106
|
-
for (var
|
|
107
|
+
for (var i = size - 1; i >= 0; i--) {
|
|
107
108
|
var _next = frontier[_diag + 1 + max],
|
|
108
109
|
_prev = frontier[_diag - 1 + max];
|
|
109
110
|
if (_next < _prev) {
|
|
@@ -117,11 +118,11 @@ function computeDiff(fragA, fragB, range) {
|
|
|
117
118
|
y = x + _diag;
|
|
118
119
|
add(x, x + 1, y, y);
|
|
119
120
|
}
|
|
120
|
-
frontier = history[
|
|
121
|
+
frontier = history[i >> 1];
|
|
121
122
|
}
|
|
122
|
-
if (fromA > -1)
|
|
123
|
+
if (fromA > -1) _diff.push(range.slice(fromA, toA, fromB, toB));
|
|
123
124
|
return {
|
|
124
|
-
v:
|
|
125
|
+
v: _diff.reverse()
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
diag = _diag;
|
|
@@ -133,7 +134,7 @@ function computeDiff(fragA, fragB, range) {
|
|
|
133
134
|
}
|
|
134
135
|
if (size % 2 == 0) history.push(frontier.slice());
|
|
135
136
|
}
|
|
136
|
-
return [range
|
|
137
|
+
return [range];
|
|
137
138
|
}
|
|
138
139
|
var Span = function () {
|
|
139
140
|
function Span(length, data) {
|
|
@@ -299,7 +300,7 @@ try {
|
|
|
299
300
|
} catch (_) {}
|
|
300
301
|
var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
|
|
301
302
|
function isLetter(code) {
|
|
302
|
-
if (code < 128) return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >=
|
|
303
|
+
if (code < 128) return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
303
304
|
var ch = String.fromCharCode(code);
|
|
304
305
|
if (letter) return letter.test(ch);
|
|
305
306
|
return ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch);
|
|
@@ -431,32 +432,32 @@ var ChangeSet = function () {
|
|
|
431
432
|
var newChanges = mergeAll(stepChanges, this.config.combine);
|
|
432
433
|
var changes = Change.merge(this.changes, newChanges, this.config.combine);
|
|
433
434
|
var updated = changes;
|
|
434
|
-
var _loop3 = function _loop3(
|
|
435
|
-
var change = updated[
|
|
435
|
+
var _loop3 = function _loop3(_i2) {
|
|
436
|
+
var change = updated[_i2];
|
|
436
437
|
if (change.fromA == change.toA || change.fromB == change.toB || !newChanges.some(function (r) {
|
|
437
438
|
return r.toB > change.fromB && r.fromB < change.toB;
|
|
438
439
|
})) {
|
|
439
|
-
|
|
440
|
+
_i = _i2;
|
|
440
441
|
return 0;
|
|
441
442
|
}
|
|
442
443
|
var diff = computeDiff(_this.config.doc.content, newDoc.content, change, _this.config.encoder);
|
|
443
444
|
if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB) {
|
|
444
|
-
|
|
445
|
+
_i = _i2;
|
|
445
446
|
return 0;
|
|
446
447
|
}
|
|
447
448
|
if (updated == changes) updated = changes.slice();
|
|
448
449
|
if (diff.length == 1) {
|
|
449
|
-
updated[
|
|
450
|
+
updated[_i2] = diff[0];
|
|
450
451
|
} else {
|
|
451
452
|
var _updated;
|
|
452
|
-
(_updated = updated).splice.apply(_updated, [
|
|
453
|
-
|
|
453
|
+
(_updated = updated).splice.apply(_updated, [_i2, 1].concat(_toConsumableArray(diff)));
|
|
454
|
+
_i2 += diff.length - 1;
|
|
454
455
|
}
|
|
455
|
-
|
|
456
|
+
_i = _i2;
|
|
456
457
|
},
|
|
457
458
|
_ret2;
|
|
458
|
-
for (var
|
|
459
|
-
_ret2 = _loop3(
|
|
459
|
+
for (var _i = 0; _i < updated.length; _i++) {
|
|
460
|
+
_ret2 = _loop3(_i);
|
|
460
461
|
if (_ret2 === 0) continue;
|
|
461
462
|
}
|
|
462
463
|
return new ChangeSet(this.config, updated);
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ function tokens(frag, encoder, start, end, target) {
|
|
|
42
42
|
// The code below will refuse to compute a diff with more than 5000
|
|
43
43
|
// insertions or deletions, which takes about 300ms to reach on my
|
|
44
44
|
// machine. This is a safeguard against runaway computations.
|
|
45
|
-
const MAX_DIFF_SIZE =
|
|
45
|
+
const MAX_DIFF_SIZE = 2500;
|
|
46
46
|
// This obscure mess of constants computes the minimum length of an
|
|
47
47
|
// unchanged range (not at the start/end of the compared content). The
|
|
48
48
|
// idea is to make it higher in bigger replacements, so that you don't
|
|
@@ -52,11 +52,17 @@ function minUnchanged(sizeA, sizeB) {
|
|
|
52
52
|
return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
|
|
53
53
|
}
|
|
54
54
|
function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
|
|
55
|
+
// When one side is longer than our max scan distance, the algorithm
|
|
56
|
+
// will never find a diff.
|
|
57
|
+
if (Math.max(range.toA - range.fromA, range.toB, range.fromB) > MAX_DIFF_SIZE)
|
|
58
|
+
return [range];
|
|
55
59
|
let tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
|
|
56
60
|
let tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
|
|
61
|
+
return diff(tokA, tokB, range, encoder.compareTokens);
|
|
62
|
+
}
|
|
63
|
+
function diff(tokA, tokB, range, cmp) {
|
|
57
64
|
// Scan from both sides to cheaply eliminate work
|
|
58
65
|
let start = 0, endA = tokA.length, endB = tokB.length;
|
|
59
|
-
let cmp = encoder.compareTokens;
|
|
60
66
|
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start]))
|
|
61
67
|
start++;
|
|
62
68
|
if (start == tokA.length && start == tokB.length)
|
|
@@ -71,11 +77,9 @@ function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
|
|
|
71
77
|
// See https://neil.fraser.name/writing/diff/myers.pdf and
|
|
72
78
|
// https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
|
|
73
79
|
let lenA = endA - start, lenB = endB - start;
|
|
74
|
-
let max = Math.min(MAX_DIFF_SIZE, lenA + lenB)
|
|
80
|
+
let max = Math.min(MAX_DIFF_SIZE, lenA + lenB);
|
|
75
81
|
let history = [];
|
|
76
|
-
let frontier =
|
|
77
|
-
for (let len = off * 2, i = 0; i < len; i++)
|
|
78
|
-
frontier[i] = -1;
|
|
82
|
+
let frontier = new Int16Array((max + 1) * 2).fill(-1);
|
|
79
83
|
for (let size = 0; size <= max; size++) {
|
|
80
84
|
for (let diag = -size; diag <= size; diag += 2) {
|
|
81
85
|
let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max];
|
|
@@ -132,7 +136,7 @@ function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
|
|
|
132
136
|
}
|
|
133
137
|
// The loop exited, meaning the maximum amount of work was done.
|
|
134
138
|
// Just return a change spanning the entire range.
|
|
135
|
-
return [range
|
|
139
|
+
return [range];
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/**
|
|
@@ -374,7 +378,7 @@ catch (_) { }
|
|
|
374
378
|
const nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
|
|
375
379
|
function isLetter(code) {
|
|
376
380
|
if (code < 128)
|
|
377
|
-
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >=
|
|
381
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
378
382
|
let ch = String.fromCharCode(code);
|
|
379
383
|
if (letter)
|
|
380
384
|
return letter.test(ch);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosemirror-changeset",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Distills a series of editing steps into deleted and added ranges",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "git://
|
|
24
|
+
"url": "git+https://code.haverbeke.berlin/prosemirror/prosemirror-changeset.git"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"prosemirror-transform": "^1.0.0"
|
package/src/README.md
CHANGED
|
@@ -7,7 +7,7 @@ in order to do such change tracking in a halfway performant way during
|
|
|
7
7
|
live editing.
|
|
8
8
|
|
|
9
9
|
This code is licensed under an [MIT
|
|
10
|
-
licence](https://
|
|
10
|
+
licence](https://code.haverbeke.berlin/prosemirror/prosemirror-changeset/src/branch/main/LICENSE).
|
|
11
11
|
|
|
12
12
|
## Programming interface
|
|
13
13
|
|
package/src/diff.ts
CHANGED
|
@@ -64,7 +64,7 @@ function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end:
|
|
|
64
64
|
// The code below will refuse to compute a diff with more than 5000
|
|
65
65
|
// insertions or deletions, which takes about 300ms to reach on my
|
|
66
66
|
// machine. This is a safeguard against runaway computations.
|
|
67
|
-
const MAX_DIFF_SIZE =
|
|
67
|
+
const MAX_DIFF_SIZE = 2500
|
|
68
68
|
|
|
69
69
|
// This obscure mess of constants computes the minimum length of an
|
|
70
70
|
// unchanged range (not at the start/end of the compared content). The
|
|
@@ -76,12 +76,18 @@ function minUnchanged(sizeA: number, sizeB: number) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, encoder: TokenEncoder<any> = DefaultEncoder) {
|
|
79
|
+
// When one side is longer than our max scan distance, the algorithm
|
|
80
|
+
// will never find a diff.
|
|
81
|
+
if (Math.max(range.toA - range.fromA, range.toB, range.fromB) > MAX_DIFF_SIZE)
|
|
82
|
+
return [range]
|
|
79
83
|
let tokA = tokens(fragA, encoder, range.fromA, range.toA, [])
|
|
80
84
|
let tokB = tokens(fragB, encoder, range.fromB, range.toB, [])
|
|
85
|
+
return diff(tokA, tokB, range, encoder.compareTokens)
|
|
86
|
+
}
|
|
81
87
|
|
|
88
|
+
function diff<T>(tokA: T[], tokB: T[], range: Change, cmp: (a: T, b: T) => boolean): Change[] {
|
|
82
89
|
// Scan from both sides to cheaply eliminate work
|
|
83
90
|
let start = 0, endA = tokA.length, endB = tokB.length
|
|
84
|
-
let cmp = encoder.compareTokens
|
|
85
91
|
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++
|
|
86
92
|
if (start == tokA.length && start == tokB.length) return []
|
|
87
93
|
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--
|
|
@@ -95,10 +101,9 @@ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, enc
|
|
|
95
101
|
// https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
|
|
96
102
|
|
|
97
103
|
let lenA = endA - start, lenB = endB - start
|
|
98
|
-
let max = Math.min(MAX_DIFF_SIZE, lenA + lenB)
|
|
99
|
-
let history:
|
|
100
|
-
let frontier
|
|
101
|
-
for (let len = off * 2, i = 0; i < len; i++) frontier[i] = -1
|
|
104
|
+
let max = Math.min(MAX_DIFF_SIZE, lenA + lenB)
|
|
105
|
+
let history: Int16Array[] = []
|
|
106
|
+
let frontier = new Int16Array((max + 1) * 2).fill(-1)
|
|
102
107
|
|
|
103
108
|
for (let size = 0; size <= max; size++) {
|
|
104
109
|
for (let diag = -size; diag <= size; diag += 2) {
|
|
@@ -109,7 +114,7 @@ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, enc
|
|
|
109
114
|
// Found a match
|
|
110
115
|
if (x >= lenA && y >= lenB) {
|
|
111
116
|
// Trace back through the history to build up a set of changed ranges.
|
|
112
|
-
let diff = [], minSpan = minUnchanged(endA - start, endB - start)
|
|
117
|
+
let diff: Change[] = [], minSpan = minUnchanged(endA - start, endB - start)
|
|
113
118
|
// Used to add steps to a diff one at a time, back to front, merging
|
|
114
119
|
// ones that are less than minSpan tokens apart
|
|
115
120
|
let fromA = -1, toA = -1, fromB = -1, toB = -1
|
|
@@ -147,5 +152,5 @@ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, enc
|
|
|
147
152
|
}
|
|
148
153
|
// The loop exited, meaning the maximum amount of work was done.
|
|
149
154
|
// Just return a change spanning the entire range.
|
|
150
|
-
return [range
|
|
155
|
+
return [range]
|
|
151
156
|
}
|
package/src/simplify.ts
CHANGED
|
@@ -12,7 +12,7 @@ const nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u304
|
|
|
12
12
|
|
|
13
13
|
function isLetter(code: number) {
|
|
14
14
|
if (code < 128)
|
|
15
|
-
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >=
|
|
15
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122
|
|
16
16
|
let ch = String.fromCharCode(code)
|
|
17
17
|
if (letter) return letter.test(ch)
|
|
18
18
|
return ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)
|
package/test/test-simplify.ts
CHANGED
|
@@ -25,6 +25,9 @@ describe("simplifyChanges", () => {
|
|
|
25
25
|
it("doesn't expand across non-word text", () => test(
|
|
26
26
|
[[7, 10]], doc(p("one two ----- four")), [[5, 10]]))
|
|
27
27
|
|
|
28
|
+
it("doesn't expand replacements across bracket characters", () => test(
|
|
29
|
+
[[7, 10]], doc(p("one two [SFX] four")), [[5, 10]]))
|
|
30
|
+
|
|
28
31
|
it("treats leaf nodes as non-words", () => test(
|
|
29
32
|
[[2, 3], [6, 7]], doc(p("one", img(), "two")), [[2, 3], [6, 7]]))
|
|
30
33
|
|