🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

prosemirror-changeset

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

prosemirror-changeset - npm Package Compare versions

Comparing version
2.2.1
to
2.3.0
+6
-0
CHANGELOG.md

@@ -0,1 +1,7 @@

## 2.3.0 (2025-05-05)
### New features
Change sets can now be passed a custom token encoder that controls the way changed content is diffed.
## 2.2.1 (2023-05-17)

@@ -2,0 +8,0 @@

+120
-252
'use strict';
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
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."); }
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); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
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; }
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; }
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); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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); } }
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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
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); }
Object.defineProperty(exports, '__esModule', {
value: true
});
function tokens(frag, start, end, target) {
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
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); }
var DefaultEncoder = {
encodeCharacter: function encodeCharacter(_char) {
return _char;
},
encodeNodeStart: function encodeNodeStart(node) {
return node.type.name;
},
encodeNodeEnd: function encodeNodeEnd() {
return -1;
},
compareTokens: function compareTokens(a, b) {
return a === b;
}
};
function tokens(frag, encoder, start, end, target) {
for (var i = 0, off = 0; i < frag.childCount; i++) {
var child = frag.child(i),
endOff = off + child.nodeSize;
endOff = off + child.nodeSize;
var from = Math.max(off, start),
to = Math.min(endOff, end);
to = Math.min(endOff, end);
if (from < to) {
if (child.isText) {
for (var j = from; j < to; j++) {
target.push(child.text.charCodeAt(j - off));
}
for (var j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text.charCodeAt(j - off), child.marks));
} else if (child.isLeaf) {
target.push(child.type.name);
target.push(encoder.encodeNodeStart(child));
} else {
if (from == off) target.push(child.type.name);
tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
if (to == endOff) target.push(-1);
if (from == off) target.push(encoder.encodeNodeStart(child));
tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
if (to == endOff) target.push(encoder.encodeNodeEnd(child));
}
}
off = endOff;
}
return target;
}
var MAX_DIFF_SIZE = 5000;
function minUnchanged(sizeA, sizeB) {
return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
}
function computeDiff(fragA, fragB, range) {
var tokA = tokens(fragA, range.fromA, range.toA, []);
var tokB = tokens(fragB, range.fromB, range.toB, []);
var encoder = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DefaultEncoder;
var tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
var tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
var start = 0,
endA = tokA.length,
endB = tokB.length;
while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start]) {
start++;
}
endA = tokA.length,
endB = tokB.length;
var cmp = encoder.compareTokens;
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++;
if (start == tokA.length && start == tokB.length) return [];
while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1]) {
endA--, endB--;
}
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--;
if (endA == start || endB == start || endA == endB && endA == start + 1) return [range.slice(start, endA, start, endB)];
var lenA = endA - start,
lenB = endB - start;
lenB = endB - start;
var max = Math.min(MAX_DIFF_SIZE, lenA + lenB),
off = max + 1;
off = max + 1;
var history = [];
var frontier = [];
for (var len = off * 2, i = 0; i < len; i++) {
frontier[i] = -1;
}
for (var len = off * 2, i = 0; i < len; i++) frontier[i] = -1;
for (var size = 0; size <= max; size++) {
for (var diag = -size; diag <= size; diag += 2) {
var next = frontier[diag + 1 + max],
prev = frontier[diag - 1 + max];
var x = next < prev ? prev : next + 1,
y = x + diag;
while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y]) {
x++, y++;
}
frontier[diag + max] = x;
if (x >= lenA && y >= lenB) {
var _ret = function () {
var _loop = function _loop(_diag) {
var next = frontier[_diag + 1 + max],
prev = frontier[_diag - 1 + max];
var x = next < prev ? prev : next + 1,
y = x + _diag;
while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++;
frontier[_diag + max] = x;
if (x >= lenA && y >= lenB) {
var diff = [],
minSpan = minUnchanged(endA - start, endB - start);
minSpan = minUnchanged(endA - start, endB - start);
var fromA = -1,
toA = -1,
fromB = -1,
toB = -1;
toA = -1,
fromB = -1,
toB = -1;
var add = function add(fA, tA, fB, tB) {

@@ -123,22 +100,18 @@ if (fromA > -1 && fromA < tA + minSpan) {

};
for (var _i = size - 1; _i >= 0; _i--) {
var _next = frontier[diag + 1 + max],
_prev = frontier[diag - 1 + max];
var _next = frontier[_diag + 1 + max],
_prev = frontier[_diag - 1 + max];
if (_next < _prev) {
diag--;
_diag--;
x = _prev + start;
y = x + diag;
y = x + _diag;
add(x, x, y, y + 1);
} else {
diag++;
_diag++;
x = _next + start;
y = x + diag;
y = x + _diag;
add(x, x + 1, y, y);
}
frontier = history[_i >> 1];
}
if (fromA > -1) diff.push(range.slice(fromA, toA, fromB, toB));

@@ -148,22 +121,20 @@ return {

};
}();
if (_typeof(_ret) === "object") return _ret.v;
}
}
diag = _diag;
},
_ret;
for (var diag = -size; diag <= size; diag += 2) {
_ret = _loop(diag);
if (_ret) return _ret.v;
}
if (size % 2 == 0) history.push(frontier.slice());
}
return [range.slice(start, endA, start, endB)];
}
var Span = function () {
function Span(length, data) {
_classCallCheck(this, Span);
this.length = length;
this.data = data;
}
_createClass(Span, [{

@@ -180,6 +151,5 @@ key: "cut",

var result = [];
for (var i = 0, off = 0; off < to; i++) {
var span = spans[i],
end = off + span.length;
end = off + span.length;
var overlap = Math.min(to, end) - Math.max(from, off);

@@ -189,3 +159,2 @@ if (overlap > 0) result.push(span.cut(overlap));

}
return result;

@@ -202,7 +171,3 @@ }

result.push(new Span(a[a.length - 1].length + b[0].length, combined));
for (var i = 1; i < b.length; i++) {
result.push(b[i]);
}
for (var i = 1; i < b.length; i++) result.push(b[i]);
return result;

@@ -214,20 +179,12 @@ }

var len = 0;
for (var i = 0; i < spans.length; i++) {
len += spans[i].length;
}
for (var i = 0; i < spans.length; i++) len += spans[i].length;
return len;
}
}]);
return Span;
}();
Span.none = [];
var Change = function () {
function Change(fromA, toA, fromB, toB, deleted, inserted) {
_classCallCheck(this, Change);
this.fromA = fromA;

@@ -240,3 +197,2 @@ this.toA = toA;

}
_createClass(Change, [{

@@ -264,3 +220,2 @@ key: "lenA",

var result = [];
for (var iX = 0, iY = 0, curX = x[0], curY = y[0];;) {

@@ -275,3 +230,2 @@ if (!curX && !curY) {

var _off = iX ? x[iX - 1].toB - x[iX - 1].toA : 0;
result.push(_off == 0 ? curY : new Change(curY.fromA - _off, curY.toA - _off, curY.fromB, curY.toB, curY.deleted, curY.inserted));

@@ -282,10 +236,9 @@ curY = iY++ == y.length ? null : y[iY];

var fromA = Math.min(curX.fromA, curY.fromA - (iX ? x[iX - 1].toB - x[iX - 1].toA : 0)),
toA = fromA;
toA = fromA;
var fromB = Math.min(curY.fromB, curX.fromB + (iY ? y[iY - 1].toB - y[iY - 1].toA : 0)),
toB = fromB;
toB = fromB;
var deleted = Span.none,
inserted = Span.none;
inserted = Span.none;
var enteredX = false,
enteredY = false;
enteredY = false;
for (;;) {

@@ -296,5 +249,4 @@ var nextX = !curX ? 2e8 : pos >= curX.fromB ? curX.toB : curX.fromB;

var inX = curX && pos >= curX.fromB,
inY = curY && pos >= curY.fromA;
inY = curY && pos >= curY.fromA;
if (!inX && !inY) break;
if (inX && pos == curX.fromB && !enteredX) {

@@ -305,3 +257,2 @@ deleted = Span.join(deleted, curX.deleted, combine);

}
if (inX && !inY) {

@@ -311,3 +262,2 @@ inserted = Span.join(inserted, Span.slice(curX.inserted, pos - curX.fromB, next - curX.fromB), combine);

}
if (inY && pos == curY.fromA && !enteredY) {

@@ -318,3 +268,2 @@ inserted = Span.join(inserted, curY.inserted, combine);

}
if (inY && !inX) {

@@ -324,3 +273,2 @@ deleted = Span.join(deleted, Span.slice(curY.deleted, pos - curY.fromA, next - curY.fromA), combine);

}
if (inX && next == curX.toB) {

@@ -330,3 +278,2 @@ curX = iX++ == x.length ? null : x[iX];

}
if (inY && next == curY.toA) {

@@ -336,6 +283,4 @@ curY = iY++ == y.length ? null : y[iY];

}
pos = next;
}
if (fromA < toA || fromB < toB) result.push(new Change(fromA, toA, fromB, toB, deleted, inserted));

@@ -346,14 +291,9 @@ }

}]);
return Change;
}();
var letter;
try {
letter = new RegExp("[\\p{Alphabetic}_]", "u");
} catch (_) {}
var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
function isLetter(code) {

@@ -365,13 +305,10 @@ if (code < 128) return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 79 && code <= 122;

}
function getText(frag, start, end) {
var out = "";
function convert(frag, start, end) {
for (var i = 0, off = 0; i < frag.childCount; i++) {
var child = frag.child(i),
endOff = off + child.nodeSize;
endOff = off + child.nodeSize;
var from = Math.max(off, start),
to = Math.min(endOff, end);
to = Math.min(endOff, end);
if (from < to) {

@@ -388,30 +325,19 @@ if (child.isText) {

}
off = endOff;
}
}
convert(frag, start, end);
return out;
}
var MAX_SIMPLIFY_DISTANCE = 30;
function simplifyChanges(changes, doc) {
var result = [];
for (var i = 0; i < changes.length; i++) {
var end = changes[i].toB,
start = i;
while (i < changes.length - 1 && changes[i + 1].fromB <= end + MAX_SIMPLIFY_DISTANCE) {
end = changes[++i].toB;
}
start = i;
while (i < changes.length - 1 && changes[i + 1].fromB <= end + MAX_SIMPLIFY_DISTANCE) end = changes[++i].toB;
simplifyAdjacentChanges(changes, start, i + 1, doc, result);
}
return result;
}
function simplifyAdjacentChanges(changes, from, to, doc, target) {

@@ -421,14 +347,11 @@ var start = Math.max(0, changes[from].fromB - MAX_SIMPLIFY_DISTANCE);

var text = getText(doc.content, start, end);
for (var i = from; i < to; i++) {
var startI = i,
last = changes[i],
deleted = last.lenA,
inserted = last.lenB;
last = changes[i],
deleted = last.lenA,
inserted = last.lenB;
while (i < to - 1) {
var next = changes[i + 1],
boundary = false;
boundary = false;
var prevLetter = last.toB == end ? false : isLetter(text.charCodeAt(last.toB - 1 - start));
for (var pos = last.toB; !boundary && pos < next.fromB; pos++) {

@@ -439,3 +362,2 @@ var nextLetter = pos == end ? false : isLetter(text.charCodeAt(pos - start));

}
if (boundary) break;

@@ -447,31 +369,19 @@ deleted += next.lenA;

}
if (inserted > 0 && deleted > 0 && !(inserted == 1 && deleted == 1)) {
var _from = changes[startI].fromB,
_to = changes[i].toB;
if (_from < end && isLetter(text.charCodeAt(_from - start))) while (_from > start && isLetter(text.charCodeAt(_from - 1 - start))) {
_from--;
}
if (_to > start && isLetter(text.charCodeAt(_to - 1 - start))) while (_to < end && isLetter(text.charCodeAt(_to - start))) {
_to++;
}
_to = changes[i].toB;
if (_from < end && isLetter(text.charCodeAt(_from - start))) while (_from > start && isLetter(text.charCodeAt(_from - 1 - start))) _from--;
if (_to > start && isLetter(text.charCodeAt(_to - 1 - start))) while (_to < end && isLetter(text.charCodeAt(_to - start))) _to++;
var joined = fillChange(changes.slice(startI, i + 1), _from, _to);
var _last = target.length ? target[target.length - 1] : null;
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);
} else {
for (var j = startI; j <= i; j++) {
target.push(changes[j]);
}
for (var j = startI; j <= i; j++) target.push(changes[j]);
}
}
return changes;
}
function combine(a, b) {
return a === b ? a : null;
}
function fillChange(changes, fromB, toB) {

@@ -482,10 +392,9 @@ var fromA = changes[0].fromA - (changes[0].fromB - fromB);

var deleted = Span.none,
inserted = Span.none;
inserted = Span.none;
var delData = (changes[0].deleted.length ? changes[0].deleted : changes[0].inserted)[0].data;
var insData = (changes[0].inserted.length ? changes[0].inserted : changes[0].deleted)[0].data;
for (var posA = fromA, posB = fromB, i = 0;; i++) {
var next = i == changes.length ? null : changes[i];
var endA = next ? next.fromA : toA,
endB = next ? next.fromB : toB;
endB = next ? next.fromB : toB;
if (endA > posA) deleted = Span.join(deleted, [new Span(endA - posA, delData)], combine);

@@ -501,14 +410,10 @@ if (endB > posB) inserted = Span.join(inserted, [new Span(endB - posB, insData)], combine);

}
return new Change(fromA, toA, fromB, toB, deleted, inserted);
}
var ChangeSet = function () {
function ChangeSet(config, changes) {
_classCallCheck(this, ChangeSet);
this.config = config;
this.changes = changes;
}
_createClass(ChangeSet, [{

@@ -518,6 +423,4 @@ key: "addSteps",

var _this = this;
var stepChanges = [];
var _loop = function _loop(i) {
var _loop2 = function _loop2() {
var d = Array.isArray(data) ? data[i] : data;

@@ -530,7 +433,5 @@ var off = 0;

};
for (var i = 0; i < maps.length; i++) {
_loop(i);
_loop2();
}
if (stepChanges.length == 0) return this;

@@ -540,41 +441,30 @@ var newChanges = mergeAll(stepChanges, this.config.combine);

var updated = changes;
var _loop2 = function _loop2(_i3) {
var change = updated[_i3];
if (change.fromA == change.toA || change.fromB == change.toB || !newChanges.some(function (r) {
return r.toB > change.fromB && r.fromB < change.toB;
})) {
var _loop3 = function _loop3(_i3) {
var change = updated[_i3];
if (change.fromA == change.toA || change.fromB == change.toB || !newChanges.some(function (r) {
return r.toB > change.fromB && r.fromB < change.toB;
})) {
_i2 = _i3;
return 0;
}
var diff = computeDiff(_this.config.doc.content, newDoc.content, change, _this.config.encoder);
if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB) {
_i2 = _i3;
return 0;
}
if (updated == changes) updated = changes.slice();
if (diff.length == 1) {
updated[_i3] = diff[0];
} else {
var _updated;
(_updated = updated).splice.apply(_updated, [_i3, 1].concat(_toConsumableArray(diff)));
_i3 += diff.length - 1;
}
_i2 = _i3;
return "continue";
}
var diff = computeDiff(_this.config.doc.content, newDoc.content, change);
if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB) {
_i2 = _i3;
return "continue";
}
if (updated == changes) updated = changes.slice();
if (diff.length == 1) {
updated[_i3] = diff[0];
} else {
var _updated;
(_updated = updated).splice.apply(_updated, [_i3, 1].concat(_toConsumableArray(diff)));
_i3 += diff.length - 1;
}
_i2 = _i3;
};
},
_ret2;
for (var _i2 = 0; _i2 < updated.length; _i2++) {
var _ret2 = _loop2(_i2);
if (_ret2 === "continue") continue;
_ret2 = _loop3(_i2);
if (_ret2 === 0) continue;
}
return new ChangeSet(this.config, updated);

@@ -594,3 +484,2 @@ }

};
return new ChangeSet(this.config, this.changes.map(function (ch) {

@@ -606,10 +495,7 @@ return new Change(ch.fromA, ch.toA, ch.fromB, ch.toB, ch.deleted.map(mapSpan), ch.inserted.map(mapSpan));

var moved = touched ? touched.toB - touched.fromB - (touched.toA - touched.fromA) : 0;
function map(p) {
return !touched || p <= touched.fromA ? p : p + moved;
}
var from = touched ? touched.fromB : 2e8,
to = touched ? touched.toB : -2e8;
to = touched ? touched.toB : -2e8;
function add(start) {

@@ -620,10 +506,7 @@ var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : start;

}
var rA = this.changes,
rB = b.changes;
rB = b.changes;
for (var iA = 0, iB = 0; iA < rA.length && iB < rB.length;) {
var rangeA = rA[iA],
rangeB = rB[iB];
rangeB = rB[iB];
if (rangeA && rangeB && sameRanges(rangeA, rangeB, map)) {

@@ -640,3 +523,2 @@ iA++;

}
return from <= to ? {

@@ -653,14 +535,13 @@ from: from,

};
var tokenEncoder = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DefaultEncoder;
return new ChangeSet({
combine: combine,
doc: doc
doc: doc,
encoder: tokenEncoder
}, []);
}
}]);
return ChangeSet;
}();
ChangeSet.computeDiff = computeDiff;
function mergeAll(ranges, combine) {

@@ -673,10 +554,7 @@ var start = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;

}
function endRange(maps) {
var from = 2e8,
to = -2e8;
to = -2e8;
for (var i = 0; i < maps.length; i++) {
var map = maps[i];
if (from != 2e8) {

@@ -686,3 +564,2 @@ from = map.map(from, -1);

}
map.forEach(function (_s, _e, start, end) {

@@ -693,3 +570,2 @@ from = Math.min(from, start);

}
return from == 2e8 ? null : {

@@ -700,3 +576,2 @@ from: from,

}
function touchedRange(maps) {

@@ -715,17 +590,10 @@ var b = endRange(maps);

}
function sameRanges(a, b, map) {
return map(a.fromB) == b.fromB && map(a.toB) == b.toB && sameSpans(a.deleted, b.deleted) && sameSpans(a.inserted, b.inserted);
}
function sameSpans(a, b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i].length != b[i].length || a[i].data !== b[i].data) return false;
}
for (var i = 0; i < a.length; i++) if (a[i].length != b[i].length || a[i].data !== b[i].data) return false;
return true;
}
exports.Change = Change;

@@ -732,0 +600,0 @@ exports.ChangeSet = ChangeSet;

@@ -1,2 +0,2 @@

import { Node } from 'prosemirror-model';
import { Mark, Node } from 'prosemirror-model';
import { StepMap } from 'prosemirror-transform';

@@ -44,3 +44,3 @@

Data associated with the inserted content. Length adds up to
`this.toB - this.toA`.
`this.toB - this.fromB`.
*/

@@ -57,2 +57,34 @@ readonly inserted: readonly Span<Data>[];

/**
A token encoder can be passed when creating a `ChangeSet` in order
to influence the way the library runs its diffing algorithm. The
encoder determines how document tokens (such as nodes and
characters) are encoded and compared.
Note that both the encoding and the comparison may run a lot, and
doing non-trivial work in these functions could impact
performance.
*/
interface TokenEncoder<T> {
/**
Encode a given character, with the given marks applied.
*/
encodeCharacter(char: number, marks: readonly Mark[]): T;
/**
Encode the start of a node or, if this is a leaf node, the
entire node.
*/
encodeNodeStart(node: Node): T;
/**
Encode the end token for the given node. It is valid to encode
every end token in the same way.
*/
encodeNodeEnd(node: Node): T;
/**
Compare the given tokens. Should return true when they count as
equal.
*/
compareTokens(a: T, b: T): boolean;
}
/**
Simplifies a set of changes for presentation. This makes the

@@ -113,9 +145,15 @@ assumption that having both insertions and deletions within a word

Create a changeset with the given base object and configuration.
The `combine` function is used to compare and combine metadata—it
should return null when metadata isn't compatible, and a combined
version for a merged range when it is.
When given, a token encoder determines how document tokens are
serialized and compared when diffing the content produced by
changes. The default is to just compare nodes by name and text
by character, ignoring marks and attributes.
*/
static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data): ChangeSet<Data>;
static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
}
export { Change, ChangeSet, Span, simplifyChanges };
export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };

@@ -1,2 +0,2 @@

import { Node } from 'prosemirror-model';
import { Mark, Node } from 'prosemirror-model';
import { StepMap } from 'prosemirror-transform';

@@ -44,3 +44,3 @@

Data associated with the inserted content. Length adds up to
`this.toB - this.toA`.
`this.toB - this.fromB`.
*/

@@ -57,2 +57,34 @@ readonly inserted: readonly Span<Data>[];

/**
A token encoder can be passed when creating a `ChangeSet` in order
to influence the way the library runs its diffing algorithm. The
encoder determines how document tokens (such as nodes and
characters) are encoded and compared.
Note that both the encoding and the comparison may run a lot, and
doing non-trivial work in these functions could impact
performance.
*/
interface TokenEncoder<T> {
/**
Encode a given character, with the given marks applied.
*/
encodeCharacter(char: number, marks: readonly Mark[]): T;
/**
Encode the start of a node or, if this is a leaf node, the
entire node.
*/
encodeNodeStart(node: Node): T;
/**
Encode the end token for the given node. It is valid to encode
every end token in the same way.
*/
encodeNodeEnd(node: Node): T;
/**
Compare the given tokens. Should return true when they count as
equal.
*/
compareTokens(a: T, b: T): boolean;
}
/**
Simplifies a set of changes for presentation. This makes the

@@ -113,9 +145,15 @@ assumption that having both insertions and deletions within a word

Create a changeset with the given base object and configuration.
The `combine` function is used to compare and combine metadata—it
should return null when metadata isn't compatible, and a combined
version for a merged range when it is.
When given, a token encoder determines how document tokens are
serialized and compared when diffing the content produced by
changes. The default is to just compare nodes by name and text
by character, ignoring marks and attributes.
*/
static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data): ChangeSet<Data>;
static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
}
export { Change, ChangeSet, Span, simplifyChanges };
export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };

@@ -0,5 +1,11 @@

const DefaultEncoder = {
encodeCharacter: char => char,
encodeNodeStart: node => node.type.name,
encodeNodeEnd: () => -1,
compareTokens: (a, b) => a === b
};
// Convert the given range of a fragment to tokens, where node open
// tokens are encoded as strings holding the node name, characters as
// their character code, and node close tokens as -1.
function tokens(frag, start, end, target) {
function tokens(frag, encoder, start, end, target) {
for (let i = 0, off = 0; i < frag.childCount; i++) {

@@ -11,13 +17,13 @@ let child = frag.child(i), endOff = off + child.nodeSize;

for (let j = from; j < to; j++)
target.push(child.text.charCodeAt(j - off));
target.push(encoder.encodeCharacter(child.text.charCodeAt(j - off), child.marks));
}
else if (child.isLeaf) {
target.push(child.type.name);
target.push(encoder.encodeNodeStart(child));
}
else {
if (from == off)
target.push(child.type.name);
tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
target.push(encoder.encodeNodeStart(child));
tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
if (to == endOff)
target.push(-1);
target.push(encoder.encodeNodeEnd(child));
}

@@ -41,12 +47,13 @@ }

}
function computeDiff(fragA, fragB, range) {
let tokA = tokens(fragA, range.fromA, range.toA, []);
let tokB = tokens(fragB, range.fromB, range.toB, []);
function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
let tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
let tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
// Scan from both sides to cheaply eliminate work
let start = 0, endA = tokA.length, endB = tokB.length;
while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start])
let cmp = encoder.compareTokens;
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start]))
start++;
if (start == tokA.length && start == tokB.length)
return [];
while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1])
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1]))
endA--, endB--;

@@ -70,3 +77,3 @@ // If the result is simple _or_ too big to cheaply compute, return

let x = next < prev ? prev : next + 1, y = x + diag;
while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y])
while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y]))
x++, y++;

@@ -231,3 +238,3 @@ frontier[diag + max] = x;

Data associated with the inserted content. Length adds up to
`this.toB - this.toA`.
`this.toB - this.fromB`.
*/

@@ -556,3 +563,3 @@ inserted) {

continue;
let diff = computeDiff(this.config.doc.content, newDoc.content, change);
let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder);
// Fast path: If they are completely different, don't do anything

@@ -630,8 +637,14 @@ if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)

Create a changeset with the given base object and configuration.
The `combine` function is used to compare and combine metadata—it
should return null when metadata isn't compatible, and a combined
version for a merged range when it is.
When given, a token encoder determines how document tokens are
serialized and compared when diffing the content produced by
changes. The default is to just compare nodes by name and text
by character, ignoring marks and attributes.
*/
static create(doc, combine = (a, b) => a === b ? a : null) {
return new ChangeSet({ combine, doc }, []);
static create(doc, combine = (a, b) => a === b ? a : null, tokenEncoder = DefaultEncoder) {
return new ChangeSet({ combine, doc, encoder: tokenEncoder }, []);
}

@@ -638,0 +651,0 @@ }

{
"name": "prosemirror-changeset",
"version": "2.2.1",
"version": "2.3.0",
"description": "Distills a series of editing steps into deleted and added ranges",

@@ -32,3 +32,4 @@ "type": "module",

"prosemirror-model": "^1.0.0",
"prosemirror-test-builder": "^1.0.0"
"prosemirror-test-builder": "^1.0.0",
"builddocs": "^1.0.8"
},

@@ -38,4 +39,4 @@ "scripts": {

"prepare": "pm-buildhelper src/changeset.ts",
"build-readme": "FIXME"
"build-readme": "builddocs --format markdown --main src/README.md src/changeset.ts > README.md"
}
}

@@ -44,3 +44,3 @@ # prosemirror-changeset

Data associated with the inserted content. Length adds up to
`this.toB - this.toA`.
`this.toB - this.fromB`.

@@ -100,4 +100,5 @@ * `static `**`merge`**`<Data>(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\

* `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any) → ChangeSet`\
* `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`\
Create a changeset with the given base object and configuration.
The `combine` function is used to compare and combine metadata—it

@@ -107,3 +108,8 @@ should return null when metadata isn't compatible, and a combined

When given, a token encoder determines how document tokens are
serialized and compared when diffing the content produced by
changes. The default is to just compare nodes by name and text
by character, ignoring marks and attributes.
* **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\

@@ -117,1 +123,28 @@ Simplifies a set of changes for presentation. This makes the

### interface TokenEncoder`<T>`
A token encoder can be passed when creating a `ChangeSet` in order
to influence the way the library runs its diffing algorithm. The
encoder determines how document tokens (such as nodes and
characters) are encoded and compared.
Note that both the encoding and the comparison may run a lot, and
doing non-trivial work in these functions could impact
performance.
* **`encodeCharacter`**`(char: number, marks: readonly Mark[]) → T`\
Encode a given character, with the given marks applied.
* **`encodeNodeStart`**`(node: Node) → T`\
Encode the start of a node or, if this is a leaf node, the
entire node.
* **`encodeNodeEnd`**`(node: Node) → T`\
Encode the end token for the given node. It is valid to encode
every end token in the same way.
* **`compareTokens`**`(a: T, b: T) → boolean`\
Compare the given tokens. Should return true when they count as
equal.

@@ -69,3 +69,3 @@ /// Stores metadata for a part of a change.

/// Data associated with the inserted content. Length adds up to
/// `this.toB - this.toA`.
/// `this.toB - this.fromB`.
readonly inserted: readonly Span<Data>[]

@@ -72,0 +72,0 @@ ) {}

import {Node} from "prosemirror-model"
import {StepMap} from "prosemirror-transform"
import {computeDiff} from "./diff"
import {computeDiff, TokenEncoder, DefaultEncoder} from "./diff"
import {Change, Span} from "./change"
export {Change, Span}
export {simplifyChanges} from "./simplify"
export {TokenEncoder}

@@ -16,3 +17,7 @@ /// A change set tracks the changes to a document from a given point

/// @internal
readonly config: {doc: Node, combine: (dataA: Data, dataB: Data) => Data},
readonly config: {
doc: Node,
combine: (dataA: Data, dataB: Data) => Data,
encoder: TokenEncoder<any>
},
/// Replaced regions.

@@ -73,3 +78,3 @@ readonly changes: readonly Change<Data>[]

!newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB)) continue
let diff = computeDiff(this.config.doc.content, newDoc.content, change)
let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder)

@@ -137,7 +142,17 @@ // Fast path: If they are completely different, don't do anything

/// Create a changeset with the given base object and configuration.
///
/// The `combine` function is used to compare and combine metadata—it
/// should return null when metadata isn't compatible, and a combined
/// version for a merged range when it is.
static create<Data = any>(doc: Node, combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any) {
return new ChangeSet({combine, doc}, [])
///
/// When given, a token encoder determines how document tokens are
/// serialized and compared when diffing the content produced by
/// changes. The default is to just compare nodes by name and text
/// by character, ignoring marks and attributes.
static create<Data = any>(
doc: Node,
combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any,
tokenEncoder: TokenEncoder<any> = DefaultEncoder
) {
return new ChangeSet({combine, doc, encoder: tokenEncoder}, [])
}

@@ -144,0 +159,0 @@

@@ -1,8 +0,37 @@

import {Fragment} from "prosemirror-model"
import {Fragment, Node, Mark} from "prosemirror-model"
import {Change} from "./change"
/// A token encoder can be passed when creating a `ChangeSet` in order
/// to influence the way the library runs its diffing algorithm. The
/// encoder determines how document tokens (such as nodes and
/// characters) are encoded and compared.
///
/// Note that both the encoding and the comparison may run a lot, and
/// doing non-trivial work in these functions could impact
/// performance.
export interface TokenEncoder<T> {
/// Encode a given character, with the given marks applied.
encodeCharacter(char: number, marks: readonly Mark[]): T
/// Encode the start of a node or, if this is a leaf node, the
/// entire node.
encodeNodeStart(node: Node): T
/// Encode the end token for the given node. It is valid to encode
/// every end token in the same way.
encodeNodeEnd(node: Node): T
/// Compare the given tokens. Should return true when they count as
/// equal.
compareTokens(a: T, b: T): boolean
}
export const DefaultEncoder: TokenEncoder<number | string> = {
encodeCharacter: char => char,
encodeNodeStart: node => node.type.name,
encodeNodeEnd: () => -1,
compareTokens: (a, b) => a === b
}
// Convert the given range of a fragment to tokens, where node open
// tokens are encoded as strings holding the node name, characters as
// their character code, and node close tokens as -1.
function tokens(frag: Fragment, start: number, end: number, target: (number | string)[]) {
function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end: number, target: T[]) {
for (let i = 0, off = 0; i < frag.childCount; i++) {

@@ -13,9 +42,9 @@ let child = frag.child(i), endOff = off + child.nodeSize

if (child.isText) {
for (let j = from; j < to; j++) target.push(child.text!.charCodeAt(j - off))
for (let j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text!.charCodeAt(j - off), child.marks))
} else if (child.isLeaf) {
target.push(child.type.name)
target.push(encoder.encodeNodeStart(child))
} else {
if (from == off) target.push(child.type.name)
tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
if (to == endOff) target.push(-1)
if (from == off) target.push(encoder.encodeNodeStart(child))
tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
if (to == endOff) target.push(encoder.encodeNodeEnd(child))
}

@@ -42,11 +71,12 @@ }

export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change) {
let tokA = tokens(fragA, range.fromA, range.toA, [])
let tokB = tokens(fragB, range.fromB, range.toB, [])
export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, encoder: TokenEncoder<any> = DefaultEncoder) {
let tokA = tokens(fragA, encoder, range.fromA, range.toA, [])
let tokB = tokens(fragB, encoder, range.fromB, range.toB, [])
// Scan from both sides to cheaply eliminate work
let start = 0, endA = tokA.length, endB = tokB.length
while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start]) start++
let cmp = encoder.compareTokens
while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++
if (start == tokA.length && start == tokB.length) return []
while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1]) endA--, endB--
while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--
// If the result is simple _or_ too big to cheaply compute, return

@@ -71,3 +101,3 @@ // the remaining region as the diff

let x = next < prev ? prev : next + 1, y = x + diag
while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y]) x++, y++
while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++
frontier[diag + max] = x

@@ -74,0 +104,0 @@ // Found a match

@@ -28,2 +28,4 @@ # prosemirror-changeset

@simplifyChanges
@simplifyChanges
@TokenEncoder