tiptap-python-utils 0.3.0__tar.gz → 0.5.0__tar.gz
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.
- {tiptap_python_utils-0.3.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.5.0}/PKG-INFO +23 -1
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/README.md +22 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/pyproject.toml +1 -1
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/__init__.py +2 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/__init__.py +2 -0
- tiptap_python_utils-0.5.0/src/tiptap_python_utils/codec/reader.py +95 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/content.py +18 -1
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/key.py +2 -0
- tiptap_python_utils-0.5.0/src/tiptap_python_utils/identity.py +9 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/select/selection.py +8 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/families.py +10 -1
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/fingerprint.py +3 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0/src/tiptap_python_utils.egg-info}/PKG-INFO +23 -1
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils.egg-info/SOURCES.txt +2 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_compat_imports.py +1 -0
- tiptap_python_utils-0.5.0/tests/test_generic_helpers.py +192 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_mutations.py +130 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_public_api.py +1 -0
- tiptap_python_utils-0.3.0/src/tiptap_python_utils/codec/reader.py +0 -42
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/LICENSE +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/MANIFEST.in +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/setup.cfg +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/raw.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/writer.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/kind.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/policy.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/exceptions.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/base.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/nodes.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/payload.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/registry.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/py.typed +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/select/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/identity.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tasks/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tasks/query.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/text/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/text/extract.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tree/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tree/path.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/types.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/walk/__init__.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/walk/traversal.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils.egg-info/dependency_links.txt +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils.egg-info/requires.txt +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils.egg-info/top_level.txt +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_codec_raw.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_content.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_extract.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_filter.py +0 -0
- {tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/tests/test_traverser.py +0 -0
{tiptap_python_utils-0.3.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.5.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiptap_python_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
|
|
5
5
|
Author: tiptap_python_utils contributors
|
|
6
6
|
License: MIT License
|
|
@@ -149,6 +149,22 @@ content.where_id("p1")
|
|
|
149
149
|
|
|
150
150
|
# By TipTap kind.
|
|
151
151
|
content.of(kind.PARAGRAPH)
|
|
152
|
+
|
|
153
|
+
# By an arbitrary predicate over every node (and its descendants).
|
|
154
|
+
content.where(lambda node: getattr(node, "level", None) == 1)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Generic queries
|
|
158
|
+
|
|
159
|
+
`Selection` carries two predicate primitives that work for any kind, so you
|
|
160
|
+
don't need a bespoke `has_heading_text`-style helper per node type:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Narrow a selection further.
|
|
164
|
+
content.of(kind.HEADING).filter(lambda n: n.level == 2)
|
|
165
|
+
|
|
166
|
+
# Existence check (short-circuits).
|
|
167
|
+
content.of(kind.HEADING).any(lambda n: n.text.strip() == "Introduction")
|
|
152
168
|
```
|
|
153
169
|
|
|
154
170
|
### Atomic mutations
|
|
@@ -175,6 +191,11 @@ content.where_id("ul1").append({"type": "listItem", "attrs": {"id": "li-new"}, "
|
|
|
175
191
|
# Append a node to the document root.
|
|
176
192
|
content.append_root({"type": "paragraph", "attrs": {"id": "p2"}, "content": []})
|
|
177
193
|
|
|
194
|
+
# Build-and-append in one call — works for any kind, stamps a fresh id when
|
|
195
|
+
# none is given. Typed fields (e.g. Heading.level) hydrate correctly.
|
|
196
|
+
content.append(kind.HEADING, "New section", attrs={"level": 2})
|
|
197
|
+
content.append(kind.PARAGRAPH, "Body text", node_id="p3")
|
|
198
|
+
|
|
178
199
|
# Replace a node by id (the replacement's attrs.id must match).
|
|
179
200
|
content.replace_by_id("p1", {
|
|
180
201
|
"type": "paragraph",
|
|
@@ -281,6 +302,7 @@ from tiptap_python_utils import (
|
|
|
281
302
|
Text,
|
|
282
303
|
has_open_tasks,
|
|
283
304
|
kind,
|
|
305
|
+
new_node_id,
|
|
284
306
|
new_shared_id,
|
|
285
307
|
open_tasks,
|
|
286
308
|
text_slices,
|
|
@@ -98,6 +98,22 @@ content.where_id("p1")
|
|
|
98
98
|
|
|
99
99
|
# By TipTap kind.
|
|
100
100
|
content.of(kind.PARAGRAPH)
|
|
101
|
+
|
|
102
|
+
# By an arbitrary predicate over every node (and its descendants).
|
|
103
|
+
content.where(lambda node: getattr(node, "level", None) == 1)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Generic queries
|
|
107
|
+
|
|
108
|
+
`Selection` carries two predicate primitives that work for any kind, so you
|
|
109
|
+
don't need a bespoke `has_heading_text`-style helper per node type:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Narrow a selection further.
|
|
113
|
+
content.of(kind.HEADING).filter(lambda n: n.level == 2)
|
|
114
|
+
|
|
115
|
+
# Existence check (short-circuits).
|
|
116
|
+
content.of(kind.HEADING).any(lambda n: n.text.strip() == "Introduction")
|
|
101
117
|
```
|
|
102
118
|
|
|
103
119
|
### Atomic mutations
|
|
@@ -124,6 +140,11 @@ content.where_id("ul1").append({"type": "listItem", "attrs": {"id": "li-new"}, "
|
|
|
124
140
|
# Append a node to the document root.
|
|
125
141
|
content.append_root({"type": "paragraph", "attrs": {"id": "p2"}, "content": []})
|
|
126
142
|
|
|
143
|
+
# Build-and-append in one call — works for any kind, stamps a fresh id when
|
|
144
|
+
# none is given. Typed fields (e.g. Heading.level) hydrate correctly.
|
|
145
|
+
content.append(kind.HEADING, "New section", attrs={"level": 2})
|
|
146
|
+
content.append(kind.PARAGRAPH, "Body text", node_id="p3")
|
|
147
|
+
|
|
127
148
|
# Replace a node by id (the replacement's attrs.id must match).
|
|
128
149
|
content.replace_by_id("p1", {
|
|
129
150
|
"type": "paragraph",
|
|
@@ -230,6 +251,7 @@ from tiptap_python_utils import (
|
|
|
230
251
|
Text,
|
|
231
252
|
has_open_tasks,
|
|
232
253
|
kind,
|
|
254
|
+
new_node_id,
|
|
233
255
|
new_shared_id,
|
|
234
256
|
open_tasks,
|
|
235
257
|
text_slices,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tiptap_python_utils"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Python utilities for parsing, traversing, editing, and serializing TipTap JSON content."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -21,6 +21,7 @@ from .model import (
|
|
|
21
21
|
registry,
|
|
22
22
|
)
|
|
23
23
|
from .contract.policy import content_id, is_parseable, node_id, tiptap_id
|
|
24
|
+
from .identity import new_node_id
|
|
24
25
|
from .select import Selection
|
|
25
26
|
from .shared import SharedFamilies, fingerprint, new_shared_id
|
|
26
27
|
from .tasks import has_open_tasks, open_tasks, syncable_tasks
|
|
@@ -58,6 +59,7 @@ __all__ = [
|
|
|
58
59
|
"is_parseable",
|
|
59
60
|
"key",
|
|
60
61
|
"kind",
|
|
62
|
+
"new_node_id",
|
|
61
63
|
"new_shared_id",
|
|
62
64
|
"node_id",
|
|
63
65
|
"open_tasks",
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/__init__.py
RENAMED
|
@@ -8,6 +8,7 @@ from .raw import (
|
|
|
8
8
|
require_object,
|
|
9
9
|
)
|
|
10
10
|
from .reader import (
|
|
11
|
+
build_node,
|
|
11
12
|
read_children,
|
|
12
13
|
read_doc,
|
|
13
14
|
read_node,
|
|
@@ -16,6 +17,7 @@ from .reader import (
|
|
|
16
17
|
from .writer import dump, dumps
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
20
|
+
"build_node",
|
|
19
21
|
"dump",
|
|
20
22
|
"dumps",
|
|
21
23
|
"normalize_text",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Raw TipTap JSON → typed AST hydration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from ..contract import key, kind
|
|
8
|
+
from ..exceptions import TiptapValidationError
|
|
9
|
+
from ..model import ContentTuple, Doc, Node, registry
|
|
10
|
+
from ..model.payload import has_any_identity
|
|
11
|
+
from .raw import require_object
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Kinds that hold block/container children, never inline text directly.
|
|
15
|
+
_TEXT_REJECTING_KINDS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
kind.DOC,
|
|
18
|
+
kind.BULLET_LIST,
|
|
19
|
+
kind.ORDERED_LIST,
|
|
20
|
+
kind.TASK_LIST,
|
|
21
|
+
kind.LIST_ITEM,
|
|
22
|
+
kind.TASK_ITEM,
|
|
23
|
+
kind.BLOCKQUOTE,
|
|
24
|
+
kind.TABLE_CELL,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_doc(raw: Mapping[str, Any]) -> Doc | None:
|
|
30
|
+
"""Read a raw TipTap document root."""
|
|
31
|
+
if raw.get(key.TYPE) != kind.DOC:
|
|
32
|
+
return None
|
|
33
|
+
parsed = read_node(raw)
|
|
34
|
+
return parsed if isinstance(parsed, Doc) else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_node(raw: Mapping[str, Any]) -> Node:
|
|
38
|
+
"""Read a raw TipTap node by delegating to the registry."""
|
|
39
|
+
children = read_children(raw.get(key.CONTENT, []))
|
|
40
|
+
return registry.read(raw, children)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_children(raw_children: Any) -> ContentTuple:
|
|
44
|
+
if not isinstance(raw_children, list):
|
|
45
|
+
return ()
|
|
46
|
+
return tuple(read_node(child) for child in raw_children if isinstance(child, dict))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_node(
|
|
50
|
+
node_kind: str,
|
|
51
|
+
text: str = "",
|
|
52
|
+
*,
|
|
53
|
+
attrs: Optional[Dict[str, Any]] = None,
|
|
54
|
+
node_id: Optional[str] = None,
|
|
55
|
+
) -> Node:
|
|
56
|
+
"""Build any typed node from (kind, text, attrs) via the registry.
|
|
57
|
+
|
|
58
|
+
Constructs a minimal raw payload and hydrates it through ``read_node`` so
|
|
59
|
+
subclass-typed fields (e.g. ``Heading.level``) and ``present`` semantics
|
|
60
|
+
are derived the same way as when parsing real JSON. Pure: ``node_id`` is
|
|
61
|
+
only stamped when ``attrs`` carries no identity of its own.
|
|
62
|
+
"""
|
|
63
|
+
merged = dict(attrs or {})
|
|
64
|
+
if node_id is not None and not has_any_identity(merged):
|
|
65
|
+
merged[key.ID] = node_id
|
|
66
|
+
|
|
67
|
+
raw: Dict[str, Any] = {key.TYPE: node_kind}
|
|
68
|
+
if merged:
|
|
69
|
+
raw[key.ATTRS] = merged
|
|
70
|
+
if text:
|
|
71
|
+
_attach_text(raw, node_kind, text)
|
|
72
|
+
return read_node(raw)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _attach_text(raw: Dict[str, Any], node_kind: str, text: str) -> None:
|
|
76
|
+
if node_kind == kind.TEXT:
|
|
77
|
+
raw[key.TEXT] = text
|
|
78
|
+
elif node_kind in _TEXT_REJECTING_KINDS:
|
|
79
|
+
raise TiptapValidationError(
|
|
80
|
+
f"Node kind '{node_kind}' cannot hold inline text content"
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
raw[key.CONTENT] = [{key.TYPE: kind.TEXT, key.TEXT: text}]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_node_input(node_or_raw: Any, *, label: str) -> Node:
|
|
87
|
+
"""Read either a typed node or a raw node payload."""
|
|
88
|
+
if isinstance(node_or_raw, Node):
|
|
89
|
+
return node_or_raw
|
|
90
|
+
|
|
91
|
+
parsed = require_object(node_or_raw, label=label)
|
|
92
|
+
node = read_doc(parsed) if parsed.get(key.TYPE) == kind.DOC else read_node(parsed)
|
|
93
|
+
if node is None:
|
|
94
|
+
raise TiptapValidationError(f"{label} must be a valid TipTap node")
|
|
95
|
+
return node
|
|
@@ -5,13 +5,14 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Any, Dict, Iterator, Optional
|
|
8
|
+
from typing import Any, Callable, Dict, Iterator, Optional
|
|
9
9
|
|
|
10
10
|
from .exceptions import TiptapValidationError
|
|
11
11
|
from .types import DocumentContent
|
|
12
12
|
|
|
13
13
|
from . import codec
|
|
14
14
|
from .contract import key, kind, policy
|
|
15
|
+
from .identity import new_node_id
|
|
15
16
|
from .model import (
|
|
16
17
|
Blockquote,
|
|
17
18
|
BulletList,
|
|
@@ -118,6 +119,9 @@ class Content:
|
|
|
118
119
|
tuple(ref for ref in self.refs() if ref.node.kind == node_kind),
|
|
119
120
|
)
|
|
120
121
|
|
|
122
|
+
def where(self, pred: Callable[[Node], bool]) -> Selection:
|
|
123
|
+
return Selection(self, tuple(self.refs())).filter(pred)
|
|
124
|
+
|
|
121
125
|
def where_shared_id(self, shared_id: str) -> Selection:
|
|
122
126
|
return Selection(
|
|
123
127
|
self,
|
|
@@ -148,6 +152,19 @@ class Content:
|
|
|
148
152
|
return self
|
|
149
153
|
return Selection(self, refs).transform(families.merge)
|
|
150
154
|
|
|
155
|
+
def append(
|
|
156
|
+
self,
|
|
157
|
+
node_kind: str,
|
|
158
|
+
text: str = "",
|
|
159
|
+
*,
|
|
160
|
+
attrs: Optional[Dict[str, Any]] = None,
|
|
161
|
+
node_id: Optional[str] = None,
|
|
162
|
+
) -> "Content":
|
|
163
|
+
node = codec.build_node(
|
|
164
|
+
node_kind, text, attrs=attrs, node_id=node_id or new_node_id()
|
|
165
|
+
)
|
|
166
|
+
return self.append_root(node)
|
|
167
|
+
|
|
151
168
|
def append_root(self, node_or_raw: Any) -> "Content":
|
|
152
169
|
return self.of(kind.DOC).append(node_or_raw)
|
|
153
170
|
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/select/selection.py
RENAMED
|
@@ -43,6 +43,14 @@ class Selection:
|
|
|
43
43
|
leaves = tuple(leaf for ref in self._refs for leaf in _first_text_descendant(ref))
|
|
44
44
|
return Selection(self._content, leaves)
|
|
45
45
|
|
|
46
|
+
def filter(self, pred: Callable[[Node], bool]) -> "Selection":
|
|
47
|
+
"""Narrow the selection to refs whose node matches ``pred``."""
|
|
48
|
+
return Selection(self._content, tuple(r for r in self._refs if pred(r.node)))
|
|
49
|
+
|
|
50
|
+
def any(self, pred: Callable[[Node], bool] = lambda _node: True) -> bool:
|
|
51
|
+
"""True if any selected node matches ``pred`` (short-circuits)."""
|
|
52
|
+
return any(pred(r.node) for r in self._refs)
|
|
53
|
+
|
|
46
54
|
def text(self, value: str) -> "Content":
|
|
47
55
|
self._require_text_only("text")
|
|
48
56
|
return self._apply(lambda node: node.with_text(value))
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/families.py
RENAMED
|
@@ -52,13 +52,22 @@ class SharedFamilies:
|
|
|
52
52
|
return len(self._bodies)
|
|
53
53
|
|
|
54
54
|
def merge(self, target: Node) -> Node:
|
|
55
|
-
"""Return target rewritten from the canonical body
|
|
55
|
+
"""Return target rewritten from the canonical body.
|
|
56
|
+
|
|
57
|
+
Preserves the target's per-copy identity (local id, sharedId) and its
|
|
58
|
+
per-copy ``place`` discriminator. The family-identical ``shared`` core
|
|
59
|
+
rides along from the canonical body unchanged.
|
|
60
|
+
"""
|
|
56
61
|
canonical = self[target.shared_id]
|
|
57
62
|
raw = canonical.raw()
|
|
58
63
|
attrs = dict(raw.get(key.ATTRS, {}))
|
|
64
|
+
attrs.pop(key.PLACE, None)
|
|
59
65
|
if target.id:
|
|
60
66
|
attrs[key.ID] = target.id
|
|
61
67
|
if target.shared_id:
|
|
62
68
|
attrs[key.SHARED_ID] = target.shared_id
|
|
69
|
+
target_attrs = target.raw().get(key.ATTRS, {})
|
|
70
|
+
if key.PLACE in target_attrs:
|
|
71
|
+
attrs[key.PLACE] = target_attrs[key.PLACE]
|
|
63
72
|
raw[key.ATTRS] = attrs
|
|
64
73
|
return codec.read_node(raw)
|
|
@@ -13,6 +13,9 @@ def fingerprint(node: Node) -> str:
|
|
|
13
13
|
attrs = dict(raw.get(key.ATTRS, {}))
|
|
14
14
|
attrs.pop(key.ID, None)
|
|
15
15
|
attrs.pop(key.SHARED_ID, None)
|
|
16
|
+
attrs.pop(key.SHARED, None)
|
|
17
|
+
attrs.pop(key.PLACE, None)
|
|
18
|
+
attrs.pop(key.TASK_CANONICAL_ID, None)
|
|
16
19
|
if attrs:
|
|
17
20
|
raw[key.ATTRS] = attrs
|
|
18
21
|
else:
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0/src/tiptap_python_utils.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiptap_python_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
|
|
5
5
|
Author: tiptap_python_utils contributors
|
|
6
6
|
License: MIT License
|
|
@@ -149,6 +149,22 @@ content.where_id("p1")
|
|
|
149
149
|
|
|
150
150
|
# By TipTap kind.
|
|
151
151
|
content.of(kind.PARAGRAPH)
|
|
152
|
+
|
|
153
|
+
# By an arbitrary predicate over every node (and its descendants).
|
|
154
|
+
content.where(lambda node: getattr(node, "level", None) == 1)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Generic queries
|
|
158
|
+
|
|
159
|
+
`Selection` carries two predicate primitives that work for any kind, so you
|
|
160
|
+
don't need a bespoke `has_heading_text`-style helper per node type:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Narrow a selection further.
|
|
164
|
+
content.of(kind.HEADING).filter(lambda n: n.level == 2)
|
|
165
|
+
|
|
166
|
+
# Existence check (short-circuits).
|
|
167
|
+
content.of(kind.HEADING).any(lambda n: n.text.strip() == "Introduction")
|
|
152
168
|
```
|
|
153
169
|
|
|
154
170
|
### Atomic mutations
|
|
@@ -175,6 +191,11 @@ content.where_id("ul1").append({"type": "listItem", "attrs": {"id": "li-new"}, "
|
|
|
175
191
|
# Append a node to the document root.
|
|
176
192
|
content.append_root({"type": "paragraph", "attrs": {"id": "p2"}, "content": []})
|
|
177
193
|
|
|
194
|
+
# Build-and-append in one call — works for any kind, stamps a fresh id when
|
|
195
|
+
# none is given. Typed fields (e.g. Heading.level) hydrate correctly.
|
|
196
|
+
content.append(kind.HEADING, "New section", attrs={"level": 2})
|
|
197
|
+
content.append(kind.PARAGRAPH, "Body text", node_id="p3")
|
|
198
|
+
|
|
178
199
|
# Replace a node by id (the replacement's attrs.id must match).
|
|
179
200
|
content.replace_by_id("p1", {
|
|
180
201
|
"type": "paragraph",
|
|
@@ -281,6 +302,7 @@ from tiptap_python_utils import (
|
|
|
281
302
|
Text,
|
|
282
303
|
has_open_tasks,
|
|
283
304
|
kind,
|
|
305
|
+
new_node_id,
|
|
284
306
|
new_shared_id,
|
|
285
307
|
open_tasks,
|
|
286
308
|
text_slices,
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils.egg-info/SOURCES.txt
RENAMED
|
@@ -5,6 +5,7 @@ pyproject.toml
|
|
|
5
5
|
src/tiptap_python_utils/__init__.py
|
|
6
6
|
src/tiptap_python_utils/content.py
|
|
7
7
|
src/tiptap_python_utils/exceptions.py
|
|
8
|
+
src/tiptap_python_utils/identity.py
|
|
8
9
|
src/tiptap_python_utils/py.typed
|
|
9
10
|
src/tiptap_python_utils/types.py
|
|
10
11
|
src/tiptap_python_utils.egg-info/PKG-INFO
|
|
@@ -44,6 +45,7 @@ tests/test_compat_imports.py
|
|
|
44
45
|
tests/test_content.py
|
|
45
46
|
tests/test_extract.py
|
|
46
47
|
tests/test_filter.py
|
|
48
|
+
tests/test_generic_helpers.py
|
|
47
49
|
tests/test_mutations.py
|
|
48
50
|
tests/test_public_api.py
|
|
49
51
|
tests/test_traverser.py
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Unit tests for the generic node helpers.
|
|
2
|
+
|
|
3
|
+
Covers the cross-cutting construction/query layer:
|
|
4
|
+
``codec.build_node``, ``Content.append``, ``Content.where``,
|
|
5
|
+
``Selection.filter`` / ``Selection.any``, and ``new_node_id``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from tiptap_python_utils import (
|
|
13
|
+
Content,
|
|
14
|
+
Heading,
|
|
15
|
+
TaskItem,
|
|
16
|
+
Text,
|
|
17
|
+
TiptapValidationError,
|
|
18
|
+
key,
|
|
19
|
+
kind,
|
|
20
|
+
new_node_id,
|
|
21
|
+
)
|
|
22
|
+
from tiptap_python_utils.codec import build_node
|
|
23
|
+
|
|
24
|
+
pytestmark = [pytest.mark.unit]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# new_node_id
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_new_node_id_is_unique_and_unprefixed() -> None:
|
|
33
|
+
a, b = new_node_id(), new_node_id()
|
|
34
|
+
assert a != b
|
|
35
|
+
assert not a.startswith("shared-") # distinct from new_shared_id()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# build_node — typed-field hydration
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_build_node_hydrates_heading_level_from_attrs() -> None:
|
|
44
|
+
node = build_node(kind.HEADING, "Title", attrs={key.LEVEL: 3})
|
|
45
|
+
assert isinstance(node, Heading)
|
|
46
|
+
assert node.level == 3
|
|
47
|
+
assert node.text == "Title"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_build_node_default_has_no_content_when_text_empty() -> None:
|
|
51
|
+
node = build_node(kind.PARAGRAPH)
|
|
52
|
+
assert node.content == ()
|
|
53
|
+
assert key.CONTENT not in node.present
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_build_node_with_text_creates_single_text_child() -> None:
|
|
57
|
+
node = build_node(kind.PARAGRAPH, "hello")
|
|
58
|
+
assert len(node.content) == 1
|
|
59
|
+
child = node.content[0]
|
|
60
|
+
assert isinstance(child, Text)
|
|
61
|
+
assert child.text == "hello"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_build_node_text_kind_sets_value_not_child() -> None:
|
|
65
|
+
node = build_node(kind.TEXT, "raw")
|
|
66
|
+
assert isinstance(node, Text)
|
|
67
|
+
assert node.text == "raw"
|
|
68
|
+
assert node.content == ()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_build_node_task_item_derives_identity_from_attrs() -> None:
|
|
72
|
+
node = build_node(kind.TASK_ITEM, attrs={key.ID: "t1"})
|
|
73
|
+
assert isinstance(node, TaskItem)
|
|
74
|
+
assert node.local_task_item_id == "t1"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# build_node — identity stamping
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_build_node_stamps_node_id_when_attrs_have_no_identity() -> None:
|
|
83
|
+
node = build_node(kind.PARAGRAPH, node_id="p1")
|
|
84
|
+
assert node.attrs.get(key.ID) == "p1"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_build_node_keeps_existing_identity_over_node_id() -> None:
|
|
88
|
+
node = build_node(kind.PARAGRAPH, attrs={key.ID: "kept"}, node_id="ignored")
|
|
89
|
+
assert node.attrs.get(key.ID) == "kept"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_build_node_pure_when_no_node_id_given() -> None:
|
|
93
|
+
node = build_node(kind.PARAGRAPH, attrs={key.LEVEL: 1})
|
|
94
|
+
assert key.ID not in node.attrs
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# build_node — container rejects inline text
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize(
|
|
103
|
+
"container", [kind.BULLET_LIST, kind.ORDERED_LIST, kind.TASK_LIST, kind.DOC]
|
|
104
|
+
)
|
|
105
|
+
def test_build_node_rejects_text_on_containers(container: str) -> None:
|
|
106
|
+
with pytest.raises(TiptapValidationError):
|
|
107
|
+
build_node(container, "nope")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_build_node_round_trips_extra_fields() -> None:
|
|
111
|
+
node = build_node(kind.PARAGRAPH, "x")
|
|
112
|
+
raw = node.raw()
|
|
113
|
+
assert raw[key.TYPE] == kind.PARAGRAPH
|
|
114
|
+
assert raw[key.CONTENT][0][key.TEXT] == "x"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Content.append
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _empty() -> Content:
|
|
123
|
+
return Content.require({"type": "doc", "content": []})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_append_stamps_fresh_id_when_omitted() -> None:
|
|
127
|
+
content = _empty().append(kind.HEADING, "H", attrs={key.LEVEL: 2})
|
|
128
|
+
[heading] = content.headings
|
|
129
|
+
assert heading.attrs.get(key.ID) # non-empty
|
|
130
|
+
assert heading.level == 2
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_append_keeps_given_node_id() -> None:
|
|
134
|
+
content = _empty().append(kind.PARAGRAPH, "p", node_id="given")
|
|
135
|
+
[para] = content.paragraphs
|
|
136
|
+
assert para.attrs.get(key.ID) == "given"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_append_adds_node_to_root() -> None:
|
|
140
|
+
content = _empty().append(kind.HEADING, "Visible")
|
|
141
|
+
assert any(h.text == "Visible" for h in content.headings)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Content.where / Selection.filter / Selection.any
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _two_headings() -> Content:
|
|
150
|
+
return (
|
|
151
|
+
_empty()
|
|
152
|
+
.append(kind.HEADING, "Intro", attrs={key.LEVEL: 1})
|
|
153
|
+
.append(kind.HEADING, "Body", attrs={key.LEVEL: 2})
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_where_filters_by_predicate() -> None:
|
|
158
|
+
content = _two_headings()
|
|
159
|
+
selected = content.where(
|
|
160
|
+
lambda n: n.kind == kind.HEADING and getattr(n, "text", "") == "Body"
|
|
161
|
+
)
|
|
162
|
+
assert len(selected) == 1
|
|
163
|
+
assert selected.nodes[0].text == "Body"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_where_visits_all_refs_including_descendants() -> None:
|
|
167
|
+
content = _two_headings()
|
|
168
|
+
# Generic where() is not kind-scoped: a Heading and its Text child both
|
|
169
|
+
# expose .text == "Body".
|
|
170
|
+
selected = content.where(lambda n: getattr(n, "text", "") == "Body")
|
|
171
|
+
assert len(selected) == 2
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_selection_any_short_circuits_true() -> None:
|
|
175
|
+
content = _two_headings()
|
|
176
|
+
assert content.of(kind.HEADING).any(lambda n: n.text == "Intro") is True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_selection_any_false_on_no_match() -> None:
|
|
180
|
+
content = _two_headings()
|
|
181
|
+
assert content.of(kind.HEADING).any(lambda n: n.text == "Missing") is False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_selection_any_false_on_empty_selection() -> None:
|
|
185
|
+
content = _empty()
|
|
186
|
+
assert content.of(kind.HEADING).any() is False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_has_heading_text_via_generic_query() -> None:
|
|
190
|
+
content = _two_headings()
|
|
191
|
+
# The replacement for the old heading-specific has_heading_text:
|
|
192
|
+
assert content.of(kind.HEADING).any(lambda n: n.text.strip() == "Body")
|
|
@@ -140,6 +140,136 @@ def test_sync_shared_no_op_when_no_matching_shared_id():
|
|
|
140
140
|
assert rewritten.dump() == target.dump()
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Self-describing shared core: `shared` (family-identical) + `place` (per-copy)
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_shared_families_ignores_shared_and_place_when_fingerprinting():
|
|
149
|
+
# Two members of one family, identical body text, but differing per-copy
|
|
150
|
+
# `place` and differing `shared` core. These keys are provenance, not body,
|
|
151
|
+
# so the fingerprint must ignore them and NOT flag a conflict.
|
|
152
|
+
content = Content.require(
|
|
153
|
+
json.dumps(
|
|
154
|
+
{
|
|
155
|
+
"type": "doc",
|
|
156
|
+
"content": [
|
|
157
|
+
{
|
|
158
|
+
"type": "paragraph",
|
|
159
|
+
"attrs": {
|
|
160
|
+
"id": "n1",
|
|
161
|
+
"sharedId": "shared-1",
|
|
162
|
+
"shared": {"id": "shared-1", "topics": ["a"]},
|
|
163
|
+
"place": {"context": "dated_note", "topicId": None},
|
|
164
|
+
},
|
|
165
|
+
"content": [{"type": "text", "text": "same body"}],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"type": "paragraph",
|
|
169
|
+
"attrs": {
|
|
170
|
+
"id": "n2",
|
|
171
|
+
"sharedId": "shared-1",
|
|
172
|
+
"shared": {"id": "shared-1", "topics": ["b"]},
|
|
173
|
+
"place": {"context": "undated_note", "topicId": "t-1"},
|
|
174
|
+
},
|
|
175
|
+
"content": [{"type": "text", "text": "same body"}],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
families = content.shared_families()
|
|
183
|
+
|
|
184
|
+
assert "shared-1" in families
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_shared_families_still_rejects_real_body_divergence_despite_place():
|
|
188
|
+
# Differing `place` must not mask a genuine body difference: the text still
|
|
189
|
+
# differs, so this remains a conflict (guards against over-stripping).
|
|
190
|
+
content = Content.require(
|
|
191
|
+
json.dumps(
|
|
192
|
+
{
|
|
193
|
+
"type": "doc",
|
|
194
|
+
"content": [
|
|
195
|
+
{
|
|
196
|
+
"type": "paragraph",
|
|
197
|
+
"attrs": {
|
|
198
|
+
"id": "n1",
|
|
199
|
+
"sharedId": "shared-1",
|
|
200
|
+
"place": {"context": "dated_note", "topicId": None},
|
|
201
|
+
},
|
|
202
|
+
"content": [{"type": "text", "text": "one"}],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"type": "paragraph",
|
|
206
|
+
"attrs": {
|
|
207
|
+
"id": "n2",
|
|
208
|
+
"sharedId": "shared-1",
|
|
209
|
+
"place": {"context": "undated_note", "topicId": "t-1"},
|
|
210
|
+
},
|
|
211
|
+
"content": [{"type": "text", "text": "two"}],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
with pytest.raises(TiptapValidationError, match="Conflicting node bodies detected"):
|
|
219
|
+
content.shared_families()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_sync_shared_preserves_per_copy_place_and_carries_shared_core():
|
|
223
|
+
source = Content.require(
|
|
224
|
+
json.dumps(
|
|
225
|
+
{
|
|
226
|
+
"type": "doc",
|
|
227
|
+
"content": [
|
|
228
|
+
{
|
|
229
|
+
"type": "paragraph",
|
|
230
|
+
"attrs": {
|
|
231
|
+
"id": "source-id",
|
|
232
|
+
"sharedId": "shared-1",
|
|
233
|
+
"shared": {"id": "shared-1", "primaryTopic": "t-react"},
|
|
234
|
+
"place": {"context": "dated_note", "topicId": None},
|
|
235
|
+
},
|
|
236
|
+
"content": [{"type": "text", "text": "new"}],
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
target = Content.require(
|
|
243
|
+
json.dumps(
|
|
244
|
+
{
|
|
245
|
+
"type": "doc",
|
|
246
|
+
"content": [
|
|
247
|
+
{
|
|
248
|
+
"type": "paragraph",
|
|
249
|
+
"attrs": {
|
|
250
|
+
"id": "target-id",
|
|
251
|
+
"sharedId": "shared-1",
|
|
252
|
+
"shared": {"id": "shared-1", "primaryTopic": "STALE"},
|
|
253
|
+
"place": {"context": "undated_note", "topicId": "t-react"},
|
|
254
|
+
},
|
|
255
|
+
"content": [{"type": "text", "text": "old"}],
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
rewritten = target.sync_shared(source.shared_families())
|
|
263
|
+
node = json.loads(rewritten.dump())["content"][0]
|
|
264
|
+
|
|
265
|
+
# Body + family-identical `shared` core come from the canonical (source).
|
|
266
|
+
assert node["content"][0]["text"] == "new"
|
|
267
|
+
assert node["attrs"]["shared"]["primaryTopic"] == "t-react"
|
|
268
|
+
# Per-copy identity and `place` stay the target's own.
|
|
269
|
+
assert node["attrs"]["id"] == "target-id"
|
|
270
|
+
assert node["attrs"]["place"] == {"context": "undated_note", "topicId": "t-react"}
|
|
271
|
+
|
|
272
|
+
|
|
143
273
|
# ---------------------------------------------------------------------------
|
|
144
274
|
# Node.with_shared_id
|
|
145
275
|
# ---------------------------------------------------------------------------
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Raw TipTap JSON → typed AST hydration."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Mapping
|
|
6
|
-
|
|
7
|
-
from ..contract import key, kind
|
|
8
|
-
from ..exceptions import TiptapValidationError
|
|
9
|
-
from ..model import ContentTuple, Doc, Node, registry
|
|
10
|
-
from .raw import require_object
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def read_doc(raw: Mapping[str, Any]) -> Doc | None:
|
|
14
|
-
"""Read a raw TipTap document root."""
|
|
15
|
-
if raw.get(key.TYPE) != kind.DOC:
|
|
16
|
-
return None
|
|
17
|
-
parsed = read_node(raw)
|
|
18
|
-
return parsed if isinstance(parsed, Doc) else None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def read_node(raw: Mapping[str, Any]) -> Node:
|
|
22
|
-
"""Read a raw TipTap node by delegating to the registry."""
|
|
23
|
-
children = read_children(raw.get(key.CONTENT, []))
|
|
24
|
-
return registry.read(raw, children)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def read_children(raw_children: Any) -> ContentTuple:
|
|
28
|
-
if not isinstance(raw_children, list):
|
|
29
|
-
return ()
|
|
30
|
-
return tuple(read_node(child) for child in raw_children if isinstance(child, dict))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def read_node_input(node_or_raw: Any, *, label: str) -> Node:
|
|
34
|
-
"""Read either a typed node or a raw node payload."""
|
|
35
|
-
if isinstance(node_or_raw, Node):
|
|
36
|
-
return node_or_raw
|
|
37
|
-
|
|
38
|
-
parsed = require_object(node_or_raw, label=label)
|
|
39
|
-
node = read_doc(parsed) if parsed.get(key.TYPE) == kind.DOC else read_node(parsed)
|
|
40
|
-
if node is None:
|
|
41
|
-
raise TiptapValidationError(f"{label} must be a valid TipTap node")
|
|
42
|
-
return node
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/raw.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/codec/writer.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/kind.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/contract/policy.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/exceptions.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/base.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/nodes.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/payload.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/model/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/select/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/shared/identity.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tasks/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tasks/query.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/text/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/text/extract.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tree/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/tree/path.py
RENAMED
|
File without changes
|
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/walk/__init__.py
RENAMED
|
File without changes
|
{tiptap_python_utils-0.3.0 → tiptap_python_utils-0.5.0}/src/tiptap_python_utils/walk/traversal.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|