prosemirror-transform 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 1.12.1 (2026-09-02)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where replace step fitting could create an invalid slice, and then crash or loop forever.
6
+
7
+ ## 1.12.0 (2026-03-30)
8
+
9
+ ### Bug fixes
10
+
11
+ Handle deletions that go from the beginning of one textblock to the beginning of another specially, deleting the selected textblocks entirely.
12
+
13
+ ### New features
14
+
15
+ `ReplaceStep.MAP_BIAS` can now be adjusted to change the way insertion steps are mapped over other insertions at the same place. The default behavior remains the same for the sake of backwards compatibility.
16
+
1
17
  ## 1.11.0 (2026-01-21)
2
18
 
3
19
  ### New features
package/CONTRIBUTING.md CHANGED
@@ -12,7 +12,7 @@ Community discussion, questions, and informal bug reporting is done on the
12
12
  ## Submitting bug reports
13
13
 
14
14
  Report bugs on the
15
- [GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
15
+ [issue tracker](https://code.haverbeke.berlin/prosemirror/prosemirror/issues).
16
16
  Before reporting a bug, please read these pointers.
17
17
 
18
18
  - The issue tracker is for *bugs*, not requests for help. Questions
@@ -34,25 +34,28 @@ Before reporting a bug, please read these pointers.
34
34
 
35
35
  ## Contributing code
36
36
 
37
+ Code generated by a language model is not welcome in this project.
38
+ Please don't waste my time with it.
39
+
37
40
  If you want to make a change that involves a significant overhaul of
38
41
  the code or introduces a user-visible new feature, create an
39
- [RFC](https://github.com/ProseMirror/rfcs/) first with your proposal.
42
+ [issue](https://code.haverbeke.berlin/prosemirror/prosemirror/issues/)
43
+ first with your proposal.
40
44
 
41
- - Make sure you have a [GitHub Account](https://github.com/signup/free)
45
+ - Make sure you have a [Codeberg](https://codeberg.org/user/sign_up)
46
+ or [GitHub](https://github.com/signup/free) account.
47
+
48
+ - Use that to create a [code.haverbeke.berlin
49
+ account](https://code.haverbeke.berlin/user/login).
42
50
 
43
- - Fork the relevant repository
44
- ([how to fork a repo](https://help.github.com/articles/fork-a-repo))
51
+ - Fork the relevant repository.
45
52
 
46
53
  - Create a local checkout of the code. You can use the
47
- [main repository](https://github.com/prosemirror/prosemirror) to
54
+ [main repository](https://code.haverbeke.berlin/prosemirror/prosemirror) to
48
55
  easily check out all core modules.
49
56
 
50
57
  - Make your changes, and commit them
51
58
 
52
- - Follow the code style of the rest of the project (see below). Run
53
- `npm run lint` (in the main repository checkout) to make sure that
54
- the linter is happy.
55
-
56
59
  - If your changes are easy to test or likely to regress, add tests in
57
60
  the relevant `test/` directory. Either put them in an existing
58
61
  `test-*.js` file, if they fit there, or add a new file.
@@ -60,13 +63,13 @@ the code or introduces a user-visible new feature, create an
60
63
  - Make sure all tests pass. Run `npm run test` to verify tests pass
61
64
  (you will need Node.js v6+).
62
65
 
63
- - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
64
- Don't put more than one feature/fix in a single pull request.
66
+ - Submit a pull request. Don't put more than one feature/fix in a
67
+ single pull request.
65
68
 
66
69
  By contributing code to ProseMirror you
67
70
 
68
71
  - Agree to license the contributed code under the project's [MIT
69
- license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
72
+ license](https://code.haverbeke.berlin/prosemirror/prosemirror/blob/main/LICENSE).
70
73
 
71
74
  - Confirm that you have the right to contribute and license the code
72
75
  in question. (Either you hold all rights on the code, or the rights
@@ -86,19 +89,16 @@ By contributing code to ProseMirror you
86
89
  - Follow the surrounding code when it comes to spacing, brace
87
90
  placement, etc.
88
91
 
89
- - Brace-less single-statement bodies are encouraged (whenever they
90
- don't impact readability).
92
+ - Brace-less single-statement bodies are encouraged whenever they
93
+ don't impact readability.
91
94
 
92
- - [getdocs](https://github.com/marijnh/getdocs)-style doc comments
95
+ - [getdocs-ts](https://code.haverbeke.berlin/marijn/getdocs-ts)-style doc comments
93
96
  above items that are part of the public API.
94
97
 
95
98
  - When documenting non-public items, you can put the type after a
96
99
  single colon, so that getdocs doesn't pick it up and add it to the
97
100
  API reference.
98
101
 
99
- - The linter (`npm run lint`) complains about unused variables and
100
- functions. Prefix their names with an underscore to muffle it.
101
-
102
102
  - ProseMirror does *not* follow JSHint or JSLint prescribed style.
103
103
  Patches that try to 'fix' code to pass one of these linters will not
104
104
  be accepted.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # prosemirror-transform
2
2
 
3
- [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-transform/blob/master/CHANGELOG.md) ]
3
+ [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://code.haverbeke.berlin/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**CHANGELOG**](https://code.haverbeke.berlin/prosemirror/prosemirror-transform/src/branch/main/CHANGELOG.md) ]
4
4
 
5
5
  This is a [core module](https://prosemirror.net/docs/ref/#transform) of [ProseMirror](https://prosemirror.net).
6
6
  ProseMirror is a well-behaved rich semantic content editor based on
@@ -17,10 +17,10 @@ number of [examples](https://prosemirror.net/examples/) and the
17
17
  [documentation](https://prosemirror.net/docs/).
18
18
 
19
19
  This code is released under an
20
- [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE).
20
+ [MIT license](https://code.haverbeke.berlin/prosemirror/prosemirror/src/branch/main/LICENSE).
21
21
  There's a [forum](http://discuss.prosemirror.net) for general
22
22
  discussion and support requests, and the
23
- [Github bug tracker](https://github.com/prosemirror/prosemirror/issues)
23
+ [bug tracker](https://code.haverbeke.berlin/prosemirror/prosemirror/issues)
24
24
  is the place to report issues.
25
25
 
26
26
  We aim to be an inclusive, welcoming community. To make that explicit,
package/dist/index.cjs CHANGED
@@ -610,8 +610,8 @@ var ReplaceStep = function (_Step5) {
610
610
  }, {
611
611
  key: "map",
612
612
  value: function map(mapping) {
613
- var from = mapping.mapResult(this.from, 1),
614
- to = mapping.mapResult(this.to, -1);
613
+ var to = mapping.mapResult(this.to, -1);
614
+ var from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
615
615
  if (from.deletedAcross && to.deletedAcross) return null;
616
616
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure);
617
617
  }
@@ -650,6 +650,7 @@ var ReplaceStep = function (_Step5) {
650
650
  }]);
651
651
  return ReplaceStep;
652
652
  }(Step);
653
+ ReplaceStep.MAP_BIAS = 1;
653
654
  Step.jsonID("replace", ReplaceStep);
654
655
  var ReplaceAroundStep = function (_Step6) {
655
656
  _inherits(ReplaceAroundStep, _Step6);
@@ -1253,9 +1254,9 @@ var Fitter = function () {
1253
1254
  content = _this$unplaced.content,
1254
1255
  openStart = _this$unplaced.openStart,
1255
1256
  openEnd = _this$unplaced.openEnd;
1256
- var inner = contentAt(content, openStart);
1257
- if (!inner.childCount || inner.firstChild.isLeaf) return false;
1258
- this.unplaced = new prosemirrorModel.Slice(content, openStart + 1, Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0));
1257
+ if (maxOpen(content, -1) <= openStart) return false;
1258
+ if (this.unplaced.size > 1 && maxOpen(content, 1) > openEnd) openEnd++;
1259
+ this.unplaced = new prosemirrorModel.Slice(content, openStart + 1, openEnd);
1259
1260
  return true;
1260
1261
  }
1261
1262
  }, {
@@ -1431,6 +1432,13 @@ function invalidMarks(type, fragment, start) {
1431
1432
  function definesContent(type) {
1432
1433
  return type.spec.defining || type.spec.definingForContent;
1433
1434
  }
1435
+ function maxOpen(frag, side) {
1436
+ for (var count = 0;; count++) {
1437
+ var ch = side < 0 ? frag.firstChild : frag.lastChild;
1438
+ if (!ch || ch.isAtom) return count;
1439
+ frag = ch.content;
1440
+ }
1441
+ }
1434
1442
  function _replaceRange(tr, from, to, slice) {
1435
1443
  if (!slice.size) return tr.deleteRange(from, to);
1436
1444
  var $from = tr.doc.resolve(from),
@@ -1507,6 +1515,18 @@ function _replaceRangeWith(tr, from, to, node) {
1507
1515
  function _deleteRange(tr, from, to) {
1508
1516
  var $from = tr.doc.resolve(from),
1509
1517
  $to = tr.doc.resolve(to);
1518
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() && $from.parentOffset == 0 && $to.parentOffset == 0) {
1519
+ var shared = $from.sharedDepth(to),
1520
+ isolated = false;
1521
+ for (var d = $from.depth; d > shared; d--) if ($from.node(d).type.spec.isolating) isolated = true;
1522
+ for (var _d4 = $to.depth; _d4 > shared; _d4--) if ($to.node(_d4).type.spec.isolating) isolated = true;
1523
+ if (!isolated) {
1524
+ for (var _d5 = $from.depth; _d5 > 0 && from == $from.start(_d5); _d5--) from = $from.before(_d5);
1525
+ for (var _d6 = $to.depth; _d6 > 0 && to == $to.start(_d6); _d6--) to = $to.before(_d6);
1526
+ $from = tr.doc.resolve(from);
1527
+ $to = tr.doc.resolve(to);
1528
+ }
1529
+ }
1510
1530
  var covered = coveredDepths($from, $to);
1511
1531
  for (var i = 0; i < covered.length; i++) {
1512
1532
  var depth = covered[i],
@@ -1514,8 +1534,8 @@ function _deleteRange(tr, from, to) {
1514
1534
  if (last && depth == 0 || $from.node(depth).type.contentMatch.validEnd) return tr["delete"]($from.start(depth), $to.end(depth));
1515
1535
  if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1)))) return tr["delete"]($from.before(depth), $to.after(depth));
1516
1536
  }
1517
- for (var d = 1; d <= $from.depth && d <= $to.depth; d++) {
1518
- if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d && $from.start(d - 1) == $to.start(d - 1) && $from.node(d - 1).canReplace($from.index(d - 1), $to.index(d - 1))) return tr["delete"]($from.before(d), to);
1537
+ for (var _d7 = 1; _d7 <= $from.depth && _d7 <= $to.depth; _d7++) {
1538
+ if (from - $from.start(_d7) == $from.depth - _d7 && to > $from.end(_d7) && $to.end(_d7) - to != $to.depth - _d7 && $from.start(_d7 - 1) == $to.start(_d7 - 1) && $from.node(_d7 - 1).canReplace($from.index(_d7 - 1), $to.index(_d7 - 1))) return tr["delete"]($from.before(_d7), to);
1519
1539
  }
1520
1540
  tr["delete"](from, to);
1521
1541
  }
package/dist/index.d.cts CHANGED
@@ -704,6 +704,15 @@ declare class ReplaceStep extends Step {
704
704
  map(mapping: Mappable): ReplaceStep | null;
705
705
  merge(other: Step): ReplaceStep | null;
706
706
  toJSON(): any;
707
+ /**
708
+ By default, for backwards compatibility, an inserting step
709
+ mapped over an insertion at that same position fill move after
710
+ the inserted content. In a collaborative editing situation, that
711
+ can make redone insertions appear in unexpected places. You can
712
+ set this to -1 to make such mapping keep the step before the
713
+ insertion instead.
714
+ */
715
+ static MAP_BIAS: -1 | 1;
707
716
  }
708
717
  /**
709
718
  Replace a part of the document with a slice of content, but
package/dist/index.d.ts CHANGED
@@ -704,6 +704,15 @@ declare class ReplaceStep extends Step {
704
704
  map(mapping: Mappable): ReplaceStep | null;
705
705
  merge(other: Step): ReplaceStep | null;
706
706
  toJSON(): any;
707
+ /**
708
+ By default, for backwards compatibility, an inserting step
709
+ mapped over an insertion at that same position fill move after
710
+ the inserted content. In a collaborative editing situation, that
711
+ can make redone insertions appear in unexpected places. You can
712
+ set this to -1 to make such mapping keep the step before the
713
+ insertion instead.
714
+ */
715
+ static MAP_BIAS: -1 | 1;
707
716
  }
708
717
  /**
709
718
  Replace a part of the document with a slice of content, but
package/dist/index.js CHANGED
@@ -711,7 +711,8 @@ class ReplaceStep extends Step {
711
711
  return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to));
712
712
  }
713
713
  map(mapping) {
714
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
714
+ let to = mapping.mapResult(this.to, -1);
715
+ let from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
715
716
  if (from.deletedAcross && to.deletedAcross)
716
717
  return null;
717
718
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure);
@@ -750,6 +751,15 @@ class ReplaceStep extends Step {
750
751
  return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure);
751
752
  }
752
753
  }
754
+ /**
755
+ By default, for backwards compatibility, an inserting step
756
+ mapped over an insertion at that same position fill move after
757
+ the inserted content. In a collaborative editing situation, that
758
+ can make redone insertions appear in unexpected places. You can
759
+ set this to -1 to make such mapping keep the step before the
760
+ insertion instead.
761
+ */
762
+ ReplaceStep.MAP_BIAS = 1;
753
763
  Step.jsonID("replace", ReplaceStep);
754
764
  /**
755
765
  Replace a part of the document with a slice of content, but
@@ -1464,10 +1474,11 @@ class Fitter {
1464
1474
  }
1465
1475
  openMore() {
1466
1476
  let { content, openStart, openEnd } = this.unplaced;
1467
- let inner = contentAt(content, openStart);
1468
- if (!inner.childCount || inner.firstChild.isLeaf)
1477
+ if (maxOpen(content, -1) <= openStart)
1469
1478
  return false;
1470
- this.unplaced = new Slice(content, openStart + 1, Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0));
1479
+ if (this.unplaced.size > 1 && maxOpen(content, 1) > openEnd)
1480
+ openEnd++;
1481
+ this.unplaced = new Slice(content, openStart + 1, openEnd);
1471
1482
  return true;
1472
1483
  }
1473
1484
  dropNode() {
@@ -1637,6 +1648,14 @@ function invalidMarks(type, fragment, start) {
1637
1648
  function definesContent(type) {
1638
1649
  return type.spec.defining || type.spec.definingForContent;
1639
1650
  }
1651
+ function maxOpen(frag, side) {
1652
+ for (let count = 0;; count++) {
1653
+ let ch = side < 0 ? frag.firstChild : frag.lastChild;
1654
+ if (!ch || ch.isAtom)
1655
+ return count;
1656
+ frag = ch.content;
1657
+ }
1658
+ }
1640
1659
  function replaceRange(tr, from, to, slice) {
1641
1660
  if (!slice.size)
1642
1661
  return tr.deleteRange(from, to);
@@ -1736,6 +1755,26 @@ function replaceRangeWith(tr, from, to, node) {
1736
1755
  }
1737
1756
  function deleteRange(tr, from, to) {
1738
1757
  let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to);
1758
+ // When the deleted range spans from the start of one textblock to
1759
+ // the start of another one, move out of the start of both blocks.
1760
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() &&
1761
+ $from.parentOffset == 0 && $to.parentOffset == 0) {
1762
+ let shared = $from.sharedDepth(to), isolated = false;
1763
+ for (let d = $from.depth; d > shared; d--)
1764
+ if ($from.node(d).type.spec.isolating)
1765
+ isolated = true;
1766
+ for (let d = $to.depth; d > shared; d--)
1767
+ if ($to.node(d).type.spec.isolating)
1768
+ isolated = true;
1769
+ if (!isolated) {
1770
+ for (let d = $from.depth; d > 0 && from == $from.start(d); d--)
1771
+ from = $from.before(d);
1772
+ for (let d = $to.depth; d > 0 && to == $to.start(d); d--)
1773
+ to = $to.before(d);
1774
+ $from = tr.doc.resolve(from);
1775
+ $to = tr.doc.resolve(to);
1776
+ }
1777
+ }
1739
1778
  let covered = coveredDepths($from, $to);
1740
1779
  for (let i = 0; i < covered.length; i++) {
1741
1780
  let depth = covered[i], last = i == covered.length - 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-transform",
3
- "version": "1.11.0",
3
+ "version": "1.12.1",
4
4
  "description": "ProseMirror document transformations",
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://github.com/prosemirror/prosemirror-transform.git"
24
+ "url": "git+https://code.haverbeke.berlin/prosemirror/prosemirror-transform.git"
25
25
  },
26
26
  "dependencies": {
27
27
  "prosemirror-model": "^1.21.0"
package/src/replace.ts CHANGED
@@ -155,10 +155,9 @@ class Fitter {
155
155
 
156
156
  openMore() {
157
157
  let {content, openStart, openEnd} = this.unplaced
158
- let inner = contentAt(content, openStart)
159
- if (!inner.childCount || inner.firstChild!.isLeaf) return false
160
- this.unplaced = new Slice(content, openStart + 1,
161
- Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0))
158
+ if (maxOpen(content, -1) <= openStart) return false
159
+ if (this.unplaced.size > 1 && maxOpen(content, 1) > openEnd) openEnd++
160
+ this.unplaced = new Slice(content, openStart + 1, openEnd)
162
161
  return true
163
162
  }
164
163
 
@@ -331,6 +330,14 @@ function definesContent(type: NodeType) {
331
330
  return type.spec.defining || type.spec.definingForContent
332
331
  }
333
332
 
333
+ function maxOpen(frag: Fragment, side: -1 | 1) {
334
+ for (let count = 0;; count++) {
335
+ let ch = side < 0 ? frag.firstChild : frag.lastChild
336
+ if (!ch || ch.isAtom) return count
337
+ frag = ch.content
338
+ }
339
+ }
340
+
334
341
  export function replaceRange(tr: Transform, from: number, to: number, slice: Slice) {
335
342
  if (!slice.size) return tr.deleteRange(from, to)
336
343
 
@@ -425,6 +432,22 @@ export function replaceRangeWith(tr: Transform, from: number, to: number, node:
425
432
 
426
433
  export function deleteRange(tr: Transform, from: number, to: number) {
427
434
  let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
435
+
436
+ // When the deleted range spans from the start of one textblock to
437
+ // the start of another one, move out of the start of both blocks.
438
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() &&
439
+ $from.parentOffset == 0 && $to.parentOffset == 0) {
440
+ let shared = $from.sharedDepth(to), isolated = false
441
+ for (let d = $from.depth; d > shared; d--) if ($from.node(d).type.spec.isolating) isolated = true
442
+ for (let d = $to.depth; d > shared; d--) if ($to.node(d).type.spec.isolating) isolated = true
443
+ if (!isolated) {
444
+ for (let d = $from.depth; d > 0 && from == $from.start(d); d--) from = $from.before(d)
445
+ for (let d = $to.depth; d > 0 && to == $to.start(d); d--) to = $to.before(d)
446
+ $from = tr.doc.resolve(from)
447
+ $to = tr.doc.resolve(to)
448
+ }
449
+ }
450
+
428
451
  let covered = coveredDepths($from, $to)
429
452
  for (let i = 0; i < covered.length; i++) {
430
453
  let depth = covered[i], last = i == covered.length - 1
@@ -40,7 +40,8 @@ export class ReplaceStep extends Step {
40
40
  }
41
41
 
42
42
  map(mapping: Mappable) {
43
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
43
+ let to = mapping.mapResult(this.to, -1)
44
+ let from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1)
44
45
  if (from.deletedAcross && to.deletedAcross) return null
45
46
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure)
46
47
  }
@@ -74,6 +75,14 @@ export class ReplaceStep extends Step {
74
75
  throw new RangeError("Invalid input for ReplaceStep.fromJSON")
75
76
  return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure)
76
77
  }
78
+
79
+ /// By default, for backwards compatibility, an inserting step
80
+ /// mapped over an insertion at that same position fill move after
81
+ /// the inserted content. In a collaborative editing situation, that
82
+ /// can make redone insertions appear in unexpected places. You can
83
+ /// set this to -1 to make such mapping keep the step before the
84
+ /// insertion instead.
85
+ static MAP_BIAS: -1 | 1 = 1
77
86
  }
78
87
 
79
88
  Step.jsonID("replace", ReplaceStep)