Skip to content

Commit ffa6950

Browse files
sciyoshiclaude
andcommitted
fix: align Python translation with upstream prosemirror-model 1.18.1
Fixes translation issues up to 1.18.1, updates typing to use ty, and fixes typos. Bug fixes: - find_diff_start crashed on text nodes of different lengths (used zip(strict=True) instead of min-length loop) - find_diff_start returned wrong result for position 0 (falsy check instead of `is not None`) - Slice.max_open had inverted `isolating` check, traversing into isolating nodes when it shouldn't Identity-vs-name comparison fixes (8 occurrences): - content.py: match_type(), compatible(), find_wrapping() used string name comparison instead of identity - mark.py: eq() used name comparison instead of identity - node.py: has_markup() used name comparison instead of identity - schema.py: MarkType.excludes() used name comparison instead of identity Other translation fixes: - mark.py: to_json() no longer includes empty attrs in output - mark.py: remove_from_set() returns original set when mark not found - mark.py: eq() uses `is` instead of `==` for self-check - fragment.py: nodes_between uses direct content.size access - fragment.py: child() raises proper bounds error message - node.py: to_json() removed unnecessary deepcopy - content.py: null_from sorted descending to match upstream Typo fixes: - content.py: "missing closing patren" -> "Missing closing paren" - schema.py: "unknow mark type" -> "Unknown mark type" Tooling: - Replace mypy/pyright with ty for type checking - Loosen dev dependency version constraints - Remove dependabot.yml - Update test expectations and README for mark serialization changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d458295 commit ffa6950

File tree

22 files changed

+131
-133
lines changed

22 files changed

+131
-133
lines changed

.github/dependabot.yml

Lines changed: 0 additions & 8 deletions
This file was deleted.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ assert [step.to_json() for step in tr.steps] == [{
6666
'to': 5
6767
}, {
6868
'stepType': 'addMark',
69-
'mark': {'type': 'strong', 'attrs': {}},
69+
'mark': {'type': 'strong'},
7070
'from': 1,
7171
'to': 4
7272
}]
@@ -78,7 +78,7 @@ assert tr.doc.to_json() == {
7878
'type': 'paragraph',
7979
'content': [{
8080
'type': 'text',
81-
'marks': [{'type': 'strong', 'attrs': {}}],
81+
'marks': [{'type': 'strong'}],
8282
'text': 'Heo'
8383
}, {
8484
'type': 'text',
@@ -87,3 +87,8 @@ assert tr.doc.to_json() == {
8787
}]
8888
}
8989
```
90+
91+
## AI Disclosure
92+
93+
The initial version of this translation was written manually in 2019. AI is now
94+
used to help keep this translation up-to-date with upstream changes.

example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from typing import Any
2+
13
from prosemirror.model import Node, Schema
4+
from prosemirror.model.schema import SchemaSpec
25

3-
basic_spec = {
6+
basic_spec: SchemaSpec[Any, Any] = {
47
"nodes": {
58
"doc": {"content": "block+"},
69
"paragraph": {

prosemirror/model/content.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def parse(cls, string: str, node_types: dict[str, "NodeType"]) -> "ContentMatch"
6969

7070
def match_type(self, type: "NodeType") -> Optional["ContentMatch"]:
7171
for next in self.next:
72-
if next.type.name == type.name:
72+
if next.type == type:
7373
return next.next
7474
return None
7575

@@ -103,7 +103,7 @@ def default_type(self) -> Optional["NodeType"]:
103103
def compatible(self, other: "ContentMatch") -> bool:
104104
for i in self.next:
105105
for j in other.next:
106-
if i.type.name == j.type.name:
106+
if i.type == j.type:
107107
return True
108108
return False
109109

@@ -136,7 +136,7 @@ def search(match: ContentMatch, types: list["NodeType"]) -> Fragment | None:
136136

137137
def find_wrapping(self, target: "NodeType") -> list["NodeType"] | None:
138138
for entry in self.wrap_cache:
139-
if entry.target.name == target.name:
139+
if entry.target == target:
140140
return entry.computed
141141
computed = self.compute_wrapping(target)
142142
self.wrap_cache.append(WrapCacheEntry(target, computed))
@@ -365,7 +365,7 @@ def parse_expr_atom(
365365
if stream.eat("("):
366366
expr = parse_expr(stream)
367367
if not stream.eat(")"):
368-
stream.err("missing closing patren")
368+
stream.err("Missing closing paren")
369369
return expr
370370
elif not re.match(r"\W", cast(str, stream.next())):
371371

@@ -491,7 +491,7 @@ def scan(n: int) -> None:
491491
scan(cast(int, to))
492492

493493
scan(node)
494-
return sorted(result)
494+
return sorted(result, key=cmp_to_key(cmp))
495495

496496

497497
class DFAState(NamedTuple):

prosemirror/model/diff.py

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
from typing import TYPE_CHECKING, TypedDict
2-
3-
from prosemirror.utils import text_length
4-
5-
from . import node as pm_node
1+
from typing import TYPE_CHECKING, TypedDict, cast
62

73
if TYPE_CHECKING:
84
from prosemirror.model.fragment import Fragment
5+
from prosemirror.model.node import TextNode
96

107

118
class Diff(TypedDict):
@@ -16,39 +13,27 @@ class Diff(TypedDict):
1613
def find_diff_start(a: "Fragment", b: "Fragment", pos: int) -> int | None:
1714
i = 0
1815
while True:
19-
if a.child_count == i or b.child_count == i:
16+
if i == a.child_count or i == b.child_count:
2017
return None if a.child_count == b.child_count else pos
2118
child_a, child_b = a.child(i), b.child(i)
2219
if child_a == child_b:
2320
pos += child_a.node_size
21+
i += 1
2422
continue
2523
if not child_a.same_markup(child_b):
2624
return pos
2725
if child_a.is_text:
28-
assert isinstance(child_a, pm_node.TextNode)
29-
assert isinstance(child_b, pm_node.TextNode)
30-
if child_a.text != child_b.text:
31-
if child_b.text.startswith(child_a.text):
32-
return pos + text_length(child_a.text)
33-
if child_a.text.startswith(child_b.text):
34-
return pos + text_length(child_b.text)
35-
next_index = next(
36-
(
37-
index_a
38-
for ((index_a, char_a), (_, char_b)) in zip(
39-
enumerate(child_a.text),
40-
enumerate(child_b.text),
41-
strict=True,
42-
)
43-
if char_a != char_b
44-
),
45-
None,
46-
)
47-
if next_index is not None:
48-
return pos + next_index
26+
text_a = cast("TextNode", child_a).text
27+
text_b = cast("TextNode", child_b).text
28+
if text_a != text_b:
29+
j = 0
30+
while j < len(text_a) and j < len(text_b) and text_a[j] == text_b[j]:
31+
j += 1
32+
pos += 1
33+
return pos
4934
if child_a.content.size or child_b.content.size:
5035
inner = find_diff_start(child_a.content, child_b.content, pos + 1)
51-
if inner:
36+
if inner is not None:
5237
return inner
5338
pos += child_a.node_size
5439
i += 1
@@ -58,10 +43,7 @@ def find_diff_end(a: "Fragment", b: "Fragment", pos_a: int, pos_b: int) -> Diff
5843
i_a, i_b = a.child_count, b.child_count
5944
while True:
6045
if i_a == 0 or i_b == 0:
61-
if i_a == i_b:
62-
return None
63-
else:
64-
return {"a": pos_a, "b": pos_b}
46+
return None if i_a == i_b else {"a": pos_a, "b": pos_b}
6547
i_a -= 1
6648
i_b -= 1
6749
child_a, child_b = a.child(i_a), b.child(i_b)
@@ -75,17 +57,14 @@ def find_diff_end(a: "Fragment", b: "Fragment", pos_a: int, pos_b: int) -> Diff
7557
return {"a": pos_a, "b": pos_b}
7658

7759
if child_a.is_text:
78-
assert isinstance(child_a, pm_node.TextNode)
79-
assert isinstance(child_b, pm_node.TextNode)
80-
if child_a.text != child_b.text:
81-
same, min_size = (
82-
0,
83-
min(text_length(child_a.text), text_length(child_b.text)),
84-
)
60+
text_a = cast("TextNode", child_a).text
61+
text_b = cast("TextNode", child_b).text
62+
if text_a != text_b:
63+
same = 0
64+
min_size = min(len(text_a), len(text_b))
8565
while (
8666
same < min_size
87-
and child_a.text[text_length(child_a.text) - same - 1]
88-
== child_b.text[text_length(child_b.text) - same - 1]
67+
and text_a[len(text_a) - same - 1] == text_b[len(text_b) - same - 1]
8968
):
9069
same += 1
9170
pos_a -= 1

prosemirror/model/fragment.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Callable, Iterable, Sequence
1+
from collections.abc import Callable, Sequence
22
from typing import (
33
TYPE_CHECKING,
44
Any,
@@ -46,7 +46,7 @@ def nodes_between(
4646
if (
4747
end > from_
4848
and f(child, node_start + pos, parent, i) is not False
49-
and getattr(child.content, "size", None)
49+
and child.content.size
5050
):
5151
start = pos + 1
5252
child.nodes_between(
@@ -192,7 +192,11 @@ def child_count(self) -> int:
192192
return len(self.content)
193193

194194
def child(self, index: int) -> "Node":
195-
return self.content[index]
195+
found = self.content[index] if index < len(self.content) else None
196+
if not found:
197+
msg = f"Index {index} out of range for {self}"
198+
raise IndexError(msg)
199+
return found
196200

197201
def maybe_child(self, index: int) -> Optional["Node"]:
198202
try:
@@ -297,8 +301,8 @@ def from_(
297301
return cls.empty
298302
if isinstance(nodes, Fragment):
299303
return nodes
300-
if isinstance(nodes, Iterable):
301-
return cls.from_array(list(nodes))
304+
if isinstance(nodes, Sequence):
305+
return cls.from_array(cast(list["Node"], list(nodes)))
302306
if hasattr(nodes, "attrs"):
303307
return cls([nodes], nodes.node_size)
304308
msg = f"cannot convert {nodes!r} to a fragment"

prosemirror/model/from_dom.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,8 @@ def read_styles(self, styles: list[str]) -> tuple[list[Mark], list[Mark]] | None
721721
remove = m.add_to_set(remove)
722722
else:
723723
add = (
724-
self.parser.schema.marks[cast(str, rule.mark)]
724+
self.parser.schema
725+
.marks[cast(str, rule.mark)]
725726
.create(rule.attrs)
726727
.add_to_set(add)
727728
)
@@ -1042,7 +1043,8 @@ def textblock_from_context(self) -> NodeType | None:
10421043
d = context.depth
10431044
while d >= 0:
10441045
default = (
1045-
context.node(d)
1046+
context
1047+
.node(d)
10461048
.content_match_at(context.index_after(d))
10471049
.default_type
10481050
)
@@ -1230,7 +1232,7 @@ def get_node_type(element: DOMNode) -> int:
12301232

12311233

12321234
def from_html(schema: Schema[Any, Any], html: str) -> JSONDict:
1233-
fragment = lxml.html.fragment_fromstring(html, create_parent="document-fragment") # type: ignore[arg-type]
1235+
fragment = lxml.html.fragment_fromstring(html, create_parent="document-fragment")
12341236

12351237
prose_doc = DOMParser.from_schema(schema).parse(fragment)
12361238

prosemirror/model/mark.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import copy
21
from typing import TYPE_CHECKING, Any, Final, Union, cast
32

43
from prosemirror.utils import Attrs, JSONDict
@@ -41,18 +40,24 @@ def add_to_set(self, set: list["Mark"]) -> list["Mark"]:
4140
return copy
4241

4342
def remove_from_set(self, set: list["Mark"]) -> list["Mark"]:
44-
return [item for item in set if not item.eq(self)]
43+
for i in range(len(set)):
44+
if self.eq(set[i]):
45+
return set[:i] + set[i + 1 :]
46+
return set
4547

4648
def is_in_set(self, set: list["Mark"]) -> bool:
4749
return any(item.eq(self) for item in set)
4850

4951
def eq(self, other: "Mark") -> bool:
50-
if self == other:
52+
if self is other:
5153
return True
52-
return self.type.name == other.type.name and self.attrs == other.attrs
54+
return self.type == other.type and self.attrs == other.attrs
5355

5456
def to_json(self) -> JSONDict:
55-
return {"type": self.type.name, "attrs": copy.deepcopy(self.attrs)}
57+
obj: JSONDict = {"type": self.type.name}
58+
if self.attrs:
59+
obj = {**obj, "attrs": self.attrs}
60+
return obj
5661

5762
@classmethod
5863
def from_json(

prosemirror/model/node.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import copy
21
from collections.abc import Callable
32
from typing import TYPE_CHECKING, Any, Optional, TypedDict, TypeGuard, Union, cast
43

@@ -110,9 +109,9 @@ def has_markup(
110109
marks: list[Mark] | None = None,
111110
) -> bool:
112111
return (
113-
self.type.name == type.name
114-
and (compare_deep(self.attrs, attrs or type.default_attrs or empty_attrs))
115-
and (Mark.same_set(self.marks, marks or Mark.none))
112+
self.type == type
113+
and compare_deep(self.attrs, attrs or type.default_attrs or empty_attrs)
114+
and Mark.same_set(self.marks, marks or Mark.none)
116115
)
117116

118117
def copy(self, content: Fragment | None = None) -> "Node":
@@ -323,20 +322,11 @@ def iteratee(node: "Node", offset: int, index: int) -> None:
323322
def to_json(self) -> JSONDict:
324323
obj: JSONDict = {"type": self.type.name}
325324
if self.attrs:
326-
obj = {
327-
**obj,
328-
"attrs": copy.deepcopy(self.attrs),
329-
}
330-
if getattr(self.content, "size", None):
331-
obj = {
332-
**obj,
333-
"content": self.content.to_json(),
334-
}
325+
obj = {**obj, "attrs": self.attrs}
326+
if self.content.size:
327+
obj = {**obj, "content": self.content.to_json()}
335328
if len(self.marks):
336-
obj = {
337-
**obj,
338-
"marks": [n.to_json() for n in self.marks],
339-
}
329+
obj = {**obj, "marks": [n.to_json() for n in self.marks]}
340330
return obj
341331

342332
@classmethod

prosemirror/model/replace.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,15 @@ def max_open(cls, fragment: Fragment, open_isolating: bool = True) -> "Slice":
130130
open_start = 0
131131
open_end = 0
132132
n = fragment.first_child
133-
while n and not n.is_leaf and (open_isolating or n.type.spec.get("isolating")):
133+
while (
134+
n and not n.is_leaf and (open_isolating or not n.type.spec.get("isolating"))
135+
):
134136
open_start += 1
135137
n = n.first_child
136138
n = fragment.last_child
137-
while n and not n.is_leaf and (open_isolating or n.type.spec.get("isolating")):
139+
while (
140+
n and not n.is_leaf and (open_isolating or not n.type.spec.get("isolating"))
141+
):
138142
open_end += 1
139143
n = n.last_child
140144
return cls(fragment, open_start, open_end)
@@ -176,7 +180,8 @@ def replace_outer(
176180
content = parent.content
177181
return close(
178182
parent,
179-
content.cut(0, from_.parent_offset)
183+
content
184+
.cut(0, from_.parent_offset)
180185
.append(slice.content)
181186
.append(content.cut(to.parent_offset)),
182187
)

0 commit comments

Comments
 (0)