Skip to content

Commit ce80876

Browse files
sciyoshiclaude
andcommitted
fix: sync prosemirror-test-builder to 1.1.1
- Fix mark deduplication bug (1.0.6): marks of same type with different attributes are no longer incorrectly deduplicated - Fix block() attrs mutation bug: use new _take_attrs helper that creates a merged dict instead of mutating the original - Restructure flatten() to match upstream ordering (strings first) - Fix mark() type annotation from NodeType to MarkType - Add translated test file for mark deduplication (test-marks.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d7e9d6a commit ce80876

File tree

4 files changed

+91
-48
lines changed

4 files changed

+91
-48
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This package provides Python implementations of the following
1111

1212
- [`prosemirror-model`](https://github.com/ProseMirror/prosemirror-model) version 1.25.4
1313
- [`prosemirror-transform`](https://github.com/ProseMirror/prosemirror-transform) version 1.8.0
14-
- [`prosemirror-test-builder`](https://github.com/ProseMirror/prosemirror-test-builder)
14+
- [`prosemirror-test-builder`](https://github.com/ProseMirror/prosemirror-test-builder) version 1.1.1
1515
- [`prosemirror-schema-basic`](https://github.com/ProseMirror/prosemirror-schema-basic) version 1.2.4
1616
- [`prosemirror-schema-list`](https://github.com/ProseMirror/prosemirror-schema-list) version 1.5.1 (node specs and `wrapRangeInList` only; command functions that depend on `prosemirror-state` are excluded)
1717

prosemirror/test_builder/build.py

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections.abc import Callable
66
from typing import Any
77

8-
from prosemirror.model import Node, NodeType, Schema
8+
from prosemirror.model import MarkType, Node, NodeType, Schema
99
from prosemirror.utils import Attrs, JSONDict
1010

1111
NO_TAG = Node.tag = {}
@@ -19,16 +19,6 @@ def flatten(
1919
result, pos, tag = [], 0, NO_TAG
2020

2121
for child in children:
22-
if hasattr(child, "tag") and child.tag != NO_TAG:
23-
if tag == NO_TAG:
24-
tag = {}
25-
for id in child.tag:
26-
tag[id] = child.tag[id] + (0 if child.is_text else 1) + pos
27-
if isinstance(child, dict) and "tag" in child and child["tag"] != Node.tag:
28-
if tag == NO_TAG:
29-
tag = {}
30-
for id in child["tag"]:
31-
tag[id] = child["tag"][id] + (0 if "flat" in child else 1) + pos
3222
if isinstance(child, str):
3323
at = 0
3424
out = ""
@@ -43,35 +33,64 @@ def flatten(
4333
pos += len(child) - at
4434
if out:
4535
result.append(f(schema.text(out)))
46-
elif isinstance(child, dict) and "flat" in child:
47-
for item in child["flat"]:
48-
node = f(item)
49-
pos += node.node_size
50-
result.append(node)
51-
elif getattr(child, "flat", 0):
52-
for item in child.flat:
53-
node = f(item)
36+
else:
37+
# Merge tag info from child (Node with .tag or dict with "tag" key)
38+
child_tag = (
39+
getattr(child, "tag", NO_TAG)
40+
if isinstance(child, Node)
41+
else child.get("tag", NO_TAG)
42+
if isinstance(child, dict)
43+
else NO_TAG
44+
)
45+
if child_tag and child_tag != NO_TAG:
46+
if tag == NO_TAG:
47+
tag = {}
48+
is_flat = getattr(child, "flat", None) or (
49+
isinstance(child, dict) and "flat" in child
50+
)
51+
is_text = getattr(child, "is_text", False)
52+
for id in child_tag:
53+
tag[id] = child_tag[id] + (0 if is_flat or is_text else 1) + pos
54+
# Process flat children or single node
55+
flat = getattr(child, "flat", None) or (
56+
child.get("flat") if isinstance(child, dict) else None
57+
)
58+
if flat:
59+
for item in flat:
60+
node = f(item)
61+
pos += node.node_size
62+
result.append(node)
63+
else:
64+
node = f(child)
5465
pos += node.node_size
5566
result.append(node)
56-
else:
57-
node = f(child)
58-
pos += node.node_size
59-
result.append(node)
6067
return result, tag
6168

6269

70+
def _take_attrs(
71+
attrs: Attrs | None, args: tuple[Any, ...]
72+
) -> tuple[Attrs | None, tuple[Any, ...]]:
73+
if not args:
74+
return attrs, args
75+
a0 = args[0]
76+
if a0 and (
77+
isinstance(a0, str | Node)
78+
or getattr(a0, "flat", None)
79+
or (isinstance(a0, dict) and "flat" in a0)
80+
):
81+
return attrs, args
82+
args = args[1:]
83+
if not attrs:
84+
return a0, args
85+
if not a0:
86+
return attrs, args
87+
result = {**attrs, **a0}
88+
return result, args
89+
90+
6391
def block(type: NodeType, attrs: Attrs | None = None):
6492
def result(*args):
65-
my_attrs = attrs
66-
if (
67-
args
68-
and args[0]
69-
and not isinstance(args[0], str | Node)
70-
and not getattr(args[0], "flat", None)
71-
and "flat" not in args[0]
72-
):
73-
my_attrs.update(args[0])
74-
args = args[1:]
93+
my_attrs, args = _take_attrs(attrs, args)
7594
nodes, tag = flatten(type.schema, args, lambda x: x)
7695
node = type.create(my_attrs, nodes)
7796
if tag != NO_TAG:
@@ -85,24 +104,14 @@ def result(*args):
85104
return result
86105

87106

88-
def mark(type: NodeType, attrs: Attrs):
107+
def mark(type: MarkType, attrs: Attrs):
89108
def result(*args):
90-
my_attrs = attrs.copy()
91-
if (
92-
args
93-
and args[0]
94-
and not isinstance(args[0], str | Node)
95-
and not getattr(args[0], "flat", None)
96-
and "flat" not in args[0]
97-
):
98-
my_attrs.update(args[0])
99-
args = args[1:]
109+
my_attrs, args = _take_attrs(attrs, args)
100110
mark = type.create(my_attrs)
101111

102112
def f(n):
103-
return (
104-
n if mark.type.is_in_set(n.marks) else n.mark(mark.add_to_set(n.marks))
105-
)
113+
new_marks = mark.add_to_set(n.marks)
114+
return n.mark(new_marks) if len(new_marks) > len(n.marks) else n
106115

107116
nodes, tag = flatten(type.schema, args, f)
108117
return {"flat": nodes, "tag": tag}

tests/prosemirror_test_builder/__init__.py

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from prosemirror.model import Schema
2+
from prosemirror.test_builder import eq
3+
from prosemirror.test_builder.build import builders
4+
5+
# This schema has an "a" mark which doesn't exclude itself
6+
test_schema = Schema({
7+
"nodes": {
8+
"doc": {"content": "block+"},
9+
"p": {"content": "inline*", "group": "block"},
10+
"text": {"group": "inline"},
11+
},
12+
"marks": {
13+
"a": {"attrs": {"href": {}}, "excludes": ""},
14+
},
15+
})
16+
17+
b = builders(test_schema, {})
18+
doc = b["doc"]
19+
p = b["p"]
20+
a = b["a"]
21+
22+
23+
class TestMultipleMarks:
24+
def test_deduplicates_identical_marks(self):
25+
actual = doc(p(a({"href": "/foo"}, a({"href": "/foo"}, "click <p>here"))))
26+
expected = doc(p(a({"href": "/foo"}, "click here")))
27+
28+
assert eq(actual, expected)
29+
assert len(actual.node_at(actual.tag["p"]).marks) == 1
30+
31+
def test_marks_of_same_type_but_different_attributes_are_distinct(self):
32+
actual = doc(p(a({"href": "/foo"}, a({"href": "/bar"}, "click <p>here"))))
33+
34+
assert len(actual.node_at(actual.tag["p"]).marks) == 2

0 commit comments

Comments
 (0)