diff --git a/README.md b/README.md
index 50b3bdb..6b212f4 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ This package provides Python implementations of the following
[ProseMirror](https://prosemirror.net/) packages:
- [`prosemirror-model`](https://github.com/ProseMirror/prosemirror-model) version 1.18.1
-- [`prosemirror-transform`](https://github.com/ProseMirror/prosemirror-transform) version 1.6.0
+- [`prosemirror-transform`](https://github.com/ProseMirror/prosemirror-transform) version 1.7.1
- [`prosemirror-test-builder`](https://github.com/ProseMirror/prosemirror-test-builder)
- [`prosemirror-schema-basic`](https://github.com/ProseMirror/prosemirror-schema-basic) version 1.1.2
- [`prosemirror-schema-list`](https://github.com/ProseMirror/prosemirror-schema-list)
@@ -87,4 +87,4 @@ assert tr.doc.to_json() == {
}]
}]
}
-```
\ No newline at end of file
+```
diff --git a/poetry.lock b/poetry.lock
index 62adeaa..9978472 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "attrs"
@@ -182,14 +182,14 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "codecov"
-version = "2.1.12"
+version = "2.1.13"
description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
- {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"},
- {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"},
+ {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"},
+ {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"},
]
[package.dependencies]
@@ -725,4 +725,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8.1,<4"
-content-hash = "2d986f3721f23927f09587eee9ed4c3efdba93943a2c545e380418077f259b8a"
+content-hash = "f5c6e0791a18839add2821b9e38fa4c3c25980701713919c7038348312791f43"
diff --git a/prosemirror/transform/mark_step.py b/prosemirror/transform/mark_step.py
index eb538e2..c77afb3 100644
--- a/prosemirror/transform/mark_step.py
+++ b/prosemirror/transform/mark_step.py
@@ -153,3 +153,112 @@ def from_json(schema, json_data):
Step.json_id("removeMark", RemoveMarkStep)
+
+
+class AddNodeMarkStep(Step):
+ def __init__(self, pos, mark):
+ super().__init__()
+ self.pos = pos
+ self.mark = mark
+
+ def apply(self, doc):
+ node = doc.node_at(self.pos)
+ if not node:
+ return StepResult.fail("No node at mark step's position")
+ updated = node.type.create(node.attrs, None, self.mark.add_to_set(node.marks))
+ return StepResult.from_replace(
+ doc,
+ self.pos,
+ self.pos + 1,
+ Slice(Fragment.from_(updated), 0, 0 if node.is_leaf else 1),
+ )
+
+ def invert(self, doc):
+ node = doc.node_at(self.pos)
+ if node:
+ new_set = self.mark.add_to_set(node.marks)
+ if len(new_set) == len(node.marks):
+ for i in range(len(node.marks)):
+ if not node.marks[i].is_in_set(new_set):
+ return AddNodeMarkStep(self.pos, node.marks[i])
+ return AddNodeMarkStep(self.pos, self.mark)
+ return RemoveNodeMarkStep(self.pos, self.mark)
+
+ def map(self, mapping):
+ pos = mapping.map_result(self.pos, 1)
+ return None if pos.deleted_after else AddNodeMarkStep(pos.pos, self.mark)
+
+ def to_json(self):
+ return {
+ "stepType": "addNodeMark",
+ "pos": self.pos,
+ "mark": self.mark.to_json(),
+ }
+
+ @staticmethod
+ def from_json(schema, json_data):
+ if isinstance(json_data, str):
+ import json
+
+ json_data = json.loads(json_data)
+ if not isinstance(json_data["pos"], int):
+ raise ValueError("Invalid input for AddNodeMarkStep.from_json")
+ return AddNodeMarkStep(
+ json_data["pos"], schema.mark_from_json(json_data["mark"])
+ )
+
+
+Step.json_id("addNodeMark", AddNodeMarkStep)
+
+
+class RemoveNodeMarkStep(Step):
+ def __init__(self, pos, mark):
+ super().__init__()
+ self.pos = pos
+ self.mark = mark
+
+ def apply(self, doc):
+ node = doc.node_at(self.pos)
+ if not node:
+ return StepResult.fail("No node at mark step's position")
+ updated = node.type.create(
+ node.attrs, None, self.mark.remove_from_set(node.marks)
+ )
+ return StepResult.from_replace(
+ doc,
+ self.pos,
+ self.pos + 1,
+ Slice(Fragment.from_(updated), 0, 0 if node.is_leaf else 1),
+ )
+
+ def invert(self, doc):
+ node = doc.node_at(self.pos)
+ if not node or not self.mark.is_in_set(node.marks):
+ return self
+ return AddNodeMarkStep(self.pos, self.mark)
+
+ def map(self, mapping):
+ pos = mapping.map_result(self.pos, 1)
+ return None if pos.deleted_after else RemoveNodeMarkStep(pos.pos, self.mark)
+
+ def to_json(self):
+ return {
+ "stepType": "removeNodeMark",
+ "pos": self.pos,
+ "mark": self.mark.to_json(),
+ }
+
+ @staticmethod
+ def from_json(schema, json_data):
+ if isinstance(json_data, str):
+ import json
+
+ json_data = json.loads(json_data)
+ if not isinstance(json_data["pos"], int):
+ raise ValueError("Invalid input for RemoveNodeMarkStep.from_json")
+ return RemoveNodeMarkStep(
+ json_data["pos"], schema.mark_from_json(json_data["mark"])
+ )
+
+
+Step.json_id("removeNodeMark", RemoveNodeMarkStep)
diff --git a/prosemirror/transform/replace.py b/prosemirror/transform/replace.py
index 7507531..0a41ffd 100644
--- a/prosemirror/transform/replace.py
+++ b/prosemirror/transform/replace.py
@@ -116,8 +116,22 @@ def fit(self) -> Optional[Step]:
return None
def find_fittable(self) -> Optional[_Fittable]:
+ start_depth = self.unplaced.open_start
+ cur = self.unplaced.content
+ open_end = self.unplaced.open_end
+ for d in range(start_depth):
+ node = cur.first_child
+ if cur.child_count > 1:
+ open_end = 0
+ if node.type.spec.get("isolating") and open_end <= d:
+ start_depth = d
+ break
+ cur = node.content
+
for pass_ in [1, 2]:
- for slice_depth in range(self.unplaced.open_start, -1, -1):
+ for slice_depth in range(
+ start_depth if pass_ == 1 else self.unplaced.open_start, -1, -1
+ ):
if slice_depth:
parent = content_at(
self.unplaced.content, slice_depth - 1
diff --git a/prosemirror/transform/transform.py b/prosemirror/transform/transform.py
index a52e045..1f50351 100644
--- a/prosemirror/transform/transform.py
+++ b/prosemirror/transform/transform.py
@@ -1,11 +1,11 @@
from typing import Union
-from prosemirror.model import Fragment, MarkType, Node, NodeType, Slice
+from prosemirror.model import Fragment, Mark, MarkType, Node, NodeType, Slice
from . import replace, structure
from .attr_step import AttrStep
from .map import Mapping
-from .mark_step import AddMarkStep, RemoveMarkStep
+from .mark_step import AddMarkStep, AddNodeMarkStep, RemoveMarkStep, RemoveNodeMarkStep
from .replace import close_fragment, covered_depths, fits_trivially, replace_step
from .replace_step import ReplaceAroundStep, ReplaceStep
from .structure import can_change_type, insert_point
@@ -460,6 +460,19 @@ def set_node_markup(self, pos, type, attrs, marks=None):
def set_node_attribute(self, pos, attr, value):
return self.step(AttrStep(pos, attr, value))
+ def add_node_mark(self, pos, mark):
+ return self.step(AddNodeMarkStep(pos, mark))
+
+ def remove_node_mark(self, pos, mark):
+ if not isinstance(mark, Mark):
+ node = self.doc.node_at(pos)
+ if not node:
+ raise ValueError("No node at position " + pos)
+ mark = mark.is_in_set(node.marks)
+ if not mark:
+ return self
+ return self.step(RemoveNodeMarkStep(pos, mark))
+
def split(self, pos, depth=None, types_after=None):
if depth is None:
depth = 1
diff --git a/pyproject.toml b/pyproject.toml
index 598976e..9466db0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,7 +23,7 @@ pydash = "^5.1.2"
pandoc = "^2.3"
pytest-cov = "^4.0.0"
isort = "^5.11.4"
-codecov = "^2.1.12"
+codecov = "^2.1.13"
coverage = "^7.0.5"
ruff = "^0.0.230"
diff --git a/tests/prosemirror_transform/tests/test_trans.py b/tests/prosemirror_transform/tests/test_trans.py
index 462f28e..962c24a 100644
--- a/tests/prosemirror_transform/tests/test_trans.py
+++ b/tests/prosemirror_transform/tests/test_trans.py
@@ -949,6 +949,45 @@ def test_will_insert_filler_nodes_before_a_node_when_necessary(self):
assert tr.doc.eq(doc(h(), b(p("One"))))
+def test_keeps_isolating_nodes_together():
+ s = Schema(
+ {
+ "nodes": {
+ **schema.spec["nodes"],
+ "iso": {
+ "group": "block",
+ "content": "block+",
+ "isolating": True,
+ },
+ },
+ }
+ )
+ doc = s.node("doc", None, [s.node("paragraph", None, [s.text("one")])])
+ iso = Fragment.from_(
+ s.node("iso", None, [s.node("paragraph", None, [s.text("two")])])
+ )
+ assert (
+ Transform(doc)
+ .replace(2, 3, Slice(iso, 2, 0))
+ .doc.eq(
+ s.node(
+ "doc",
+ None,
+ [
+ s.node("paragraph", None, [s.text("o")]),
+ s.node("iso", None, [s.node("paragraph", None, [s.text("two")])]),
+ s.node("paragraph", None, [s.text("e")]),
+ ],
+ )
+ )
+ )
+ assert (
+ Transform(doc)
+ .replace(2, 3, Slice(iso, 2, 2))
+ .doc.eq(s.node("doc", None, [s.node("paragraph", None, [s.text("otwoe")])]))
+ )
+
+
@pytest.mark.parametrize(
"doc,source,expect",
[
@@ -1051,3 +1090,37 @@ def test_delete_range(doc, expect, test_transform):
doc.tag.get("a"), doc.tag.get("b") or doc.tag.get("a")
)
test_transform(tr, expect)
+
+
+@pytest.mark.parametrize(
+ "doc,mark,expect",
+ [
+ # adds a mark
+ (doc(p("", img())), schema.mark("em"), doc(p("", em(img())))),
+ # doesn't duplicate a mark
+ (doc(p("", em(img()))), schema.mark("em"), doc(p("", em(img())))),
+ # replaces a mark
+ (
+ doc(p("", a(img()))),
+ schema.mark("link", {"href": "x"}),
+ doc(p("", a({"href": "x"}, img()))),
+ ),
+ ],
+)
+def test_add_node_mark(doc, mark, expect, test_transform):
+ test_transform(Transform(doc).add_node_mark(doc.tag["a"], mark), expect)
+
+
+@pytest.mark.parametrize(
+ "doc,mark,expect",
+ [
+ # removes a mark
+ (doc(p("", em(img()))), schema.mark("em"), doc(p("", img()))),
+ # doesn't do anything when there is no mark
+ (doc(p("", img())), schema.mark("em"), doc(p("", img()))),
+ # can remove a mark from multiple marks
+ (doc(p("", em(a(img())))), schema.mark("em"), doc(p("", a(img())))),
+ ],
+)
+def test_remove_node_mark(doc, mark, expect, test_transform):
+ test_transform(Transform(doc).remove_node_mark(doc.tag["a"], mark), expect)