tiptap-python-utils 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +0 -3
- tiptap_python_utils/codec/__init__.py +5 -4
- tiptap_python_utils/codec/{json.py → raw.py} +8 -46
- tiptap_python_utils/codec/reader.py +42 -0
- tiptap_python_utils/codec/writer.py +16 -0
- tiptap_python_utils/content.py +12 -0
- tiptap_python_utils/model/__init__.py +41 -337
- tiptap_python_utils/model/base.py +136 -0
- tiptap_python_utils/model/nodes.py +127 -0
- tiptap_python_utils/model/payload.py +81 -0
- tiptap_python_utils/model/registry.py +57 -0
- tiptap_python_utils/select/selection.py +70 -9
- tiptap_python_utils/shared/__init__.py +4 -10
- tiptap_python_utils/shared/families.py +47 -0
- tiptap_python_utils/shared/fingerprint.py +21 -0
- tiptap_python_utils/shared/identity.py +48 -0
- tiptap_python_utils/shared/sync.py +63 -0
- tiptap_python_utils-0.2.0.dist-info/METADATA +315 -0
- tiptap_python_utils-0.2.0.dist-info/RECORD +38 -0
- tiptap_python_utils/edit/__init__.py +0 -12
- tiptap_python_utils/edit/commands.py +0 -163
- tiptap_python_utils/shared/service.py +0 -147
- tiptap_python_utils-0.1.0.dist-info/METADATA +0 -176
- tiptap_python_utils-0.1.0.dist-info/RECORD +0 -31
- {tiptap_python_utils-0.1.0.dist-info → tiptap_python_utils-0.2.0.dist-info}/WHEEL +0 -0
- {tiptap_python_utils-0.1.0.dist-info → tiptap_python_utils-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {tiptap_python_utils-0.1.0.dist-info → tiptap_python_utils-0.2.0.dist-info}/top_level.txt +0 -0
tiptap_python_utils/__init__.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from .contract import key, kind
|
|
4
4
|
from .content import Content
|
|
5
|
-
from .edit import append_node, replace_node
|
|
6
5
|
from .exceptions import TiptapValidationError
|
|
7
6
|
from .model import (
|
|
8
7
|
Blockquote,
|
|
@@ -60,7 +59,6 @@ __all__ = [
|
|
|
60
59
|
"TiptapValidationError",
|
|
61
60
|
"Unknown",
|
|
62
61
|
"Walker",
|
|
63
|
-
"append_node",
|
|
64
62
|
"content_id",
|
|
65
63
|
"fingerprint_shared",
|
|
66
64
|
"has_open_tasks",
|
|
@@ -72,7 +70,6 @@ __all__ = [
|
|
|
72
70
|
"node_id",
|
|
73
71
|
"open_tasks",
|
|
74
72
|
"registry",
|
|
75
|
-
"replace_node",
|
|
76
73
|
"shared_families",
|
|
77
74
|
"shared_id",
|
|
78
75
|
"stamp_shared",
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""Raw JSON codec exports."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
dump,
|
|
5
|
-
dumps,
|
|
3
|
+
from .raw import (
|
|
6
4
|
normalize_text,
|
|
7
5
|
parse_raw,
|
|
8
6
|
raw_node_id,
|
|
9
7
|
raw_text,
|
|
8
|
+
require_object,
|
|
9
|
+
)
|
|
10
|
+
from .reader import (
|
|
10
11
|
read_children,
|
|
11
12
|
read_doc,
|
|
12
13
|
read_node,
|
|
13
14
|
read_node_input,
|
|
14
|
-
require_object,
|
|
15
15
|
)
|
|
16
|
+
from .writer import dump, dumps
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"dump",
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
"""TipTap
|
|
1
|
+
"""Raw TipTap JSON I/O and dict-shaped helpers.
|
|
2
|
+
|
|
3
|
+
This module is intentionally free of any ``..model`` import so that raw
|
|
4
|
+
validation can be exercised without hydrating the typed AST.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
import json
|
|
6
10
|
from copy import deepcopy
|
|
7
|
-
from typing import Any, Dict, Mapping, Optional
|
|
11
|
+
from typing import Any, Dict, Iterator, Mapping, Optional
|
|
8
12
|
|
|
13
|
+
from ..contract import key, kind, policy
|
|
9
14
|
from ..exceptions import TiptapValidationError
|
|
10
15
|
from ..types import DocumentContent
|
|
11
16
|
|
|
12
|
-
from ..contract import key, kind, policy
|
|
13
|
-
from ..model import ContentTuple, Doc, Node, registry
|
|
14
|
-
|
|
15
17
|
|
|
16
18
|
def parse_raw(raw: Optional[DocumentContent]) -> Optional[Dict[str, Any]]:
|
|
17
19
|
"""Leniently parse a raw document payload."""
|
|
@@ -45,46 +47,6 @@ def require_object(
|
|
|
45
47
|
return parsed
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
def read_doc(raw: Mapping[str, Any]) -> Doc | None:
|
|
49
|
-
"""Read a raw TipTap document root."""
|
|
50
|
-
if raw.get(key.TYPE) != kind.DOC:
|
|
51
|
-
return None
|
|
52
|
-
parsed = read_node(raw)
|
|
53
|
-
return parsed if isinstance(parsed, Doc) else None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def read_node(raw: Mapping[str, Any]) -> Node:
|
|
57
|
-
"""Read a raw TipTap node by delegating to the registry."""
|
|
58
|
-
children = read_children(raw.get(key.CONTENT, []))
|
|
59
|
-
return registry.read(raw, children)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def read_children(raw_children: Any) -> ContentTuple:
|
|
63
|
-
if not isinstance(raw_children, list):
|
|
64
|
-
return ()
|
|
65
|
-
return tuple(read_node(child) for child in raw_children if isinstance(child, dict))
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def read_node_input(node_or_raw: Any, *, label: str) -> Node:
|
|
69
|
-
"""Read either a typed node or a raw node payload."""
|
|
70
|
-
if isinstance(node_or_raw, Node):
|
|
71
|
-
return node_or_raw
|
|
72
|
-
|
|
73
|
-
parsed = require_object(node_or_raw, label=label)
|
|
74
|
-
node = read_doc(parsed) if parsed.get(key.TYPE) == kind.DOC else read_node(parsed)
|
|
75
|
-
if node is None:
|
|
76
|
-
raise TiptapValidationError(f"{label} must be a valid TipTap node")
|
|
77
|
-
return node
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def dump(node: Node) -> Dict[str, Any]:
|
|
81
|
-
return node.raw()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def dumps(node: Node) -> str:
|
|
85
|
-
return json.dumps(dump(node))
|
|
86
|
-
|
|
87
|
-
|
|
88
50
|
def raw_node_id(node: Mapping[str, Any]) -> str:
|
|
89
51
|
return policy.content_id(node.get(key.ATTRS, {}))
|
|
90
52
|
|
|
@@ -97,7 +59,7 @@ def normalize_text(value: str) -> str:
|
|
|
97
59
|
return " ".join(value.split())
|
|
98
60
|
|
|
99
61
|
|
|
100
|
-
def _iter_text(node: Mapping[str, Any]):
|
|
62
|
+
def _iter_text(node: Mapping[str, Any]) -> Iterator[str]:
|
|
101
63
|
if not isinstance(node, dict):
|
|
102
64
|
return
|
|
103
65
|
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Typed AST → raw TipTap JSON serialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from ..model import Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def dump(node: Node) -> Dict[str, Any]:
|
|
12
|
+
return node.raw()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def dumps(node: Node) -> str:
|
|
16
|
+
return json.dumps(dump(node))
|
tiptap_python_utils/content.py
CHANGED
|
@@ -117,6 +117,18 @@ class Content:
|
|
|
117
117
|
tuple(ref for ref in self.refs() if ref.node.kind == node_kind),
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
+
def append_root(self, node_or_raw: Any) -> "Content":
|
|
121
|
+
return self.of(kind.DOC).append(node_or_raw)
|
|
122
|
+
|
|
123
|
+
def replace_by_id(self, node_id: str, node_or_raw: Any) -> "Content":
|
|
124
|
+
replacement = codec.read_node_input(node_or_raw, label="Node content")
|
|
125
|
+
replacement_id = replacement.attrs.get(key.ID, "")
|
|
126
|
+
if not replacement_id:
|
|
127
|
+
raise TiptapValidationError("Node content must include attrs.id")
|
|
128
|
+
if replacement_id != node_id:
|
|
129
|
+
raise TiptapValidationError("Node content attrs.id must match path node_id")
|
|
130
|
+
return self.where_id(node_id).replace(replacement)
|
|
131
|
+
|
|
120
132
|
def dump(self) -> str:
|
|
121
133
|
return json.dumps(self.to_dict())
|
|
122
134
|
|
|
@@ -1,345 +1,49 @@
|
|
|
1
|
-
"""Typed TipTap AST model.
|
|
1
|
+
"""Typed TipTap AST model.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package initializer is intentionally re-export-only. Implementation
|
|
4
|
+
lives in:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
- ``base`` — ``Node``, ``Text``, ``ContentTuple``, ``MarksTuple``, ``NodeT``
|
|
7
|
+
- ``nodes`` — concrete node classes (``Doc``, ``Paragraph``, ``Heading``, …)
|
|
8
|
+
- ``registry`` — ``Registry`` plus the bootstrapped ``registry`` instance
|
|
9
|
+
- ``payload`` — raw-payload extraction helpers used by node readers
|
|
10
|
+
"""
|
|
8
11
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True)
|
|
17
|
-
class Node:
|
|
18
|
-
"""Base typed TipTap node."""
|
|
19
|
-
|
|
20
|
-
id: str = ""
|
|
21
|
-
content: ContentTuple = field(default_factory=tuple)
|
|
22
|
-
attrs: Dict[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
23
|
-
extra: Dict[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
24
|
-
present: frozenset[str] = field(default_factory=frozenset, compare=False, repr=False)
|
|
25
|
-
|
|
26
|
-
kind: ClassVar[str]
|
|
27
|
-
|
|
28
|
-
def __post_init__(self) -> None:
|
|
29
|
-
if not self.id:
|
|
30
|
-
object.__setattr__(self, "id", policy.content_id(self.attrs))
|
|
31
|
-
|
|
32
|
-
@classmethod
|
|
33
|
-
def read(cls: type[NodeT], raw: Mapping[str, Any], children: ContentTuple) -> NodeT:
|
|
34
|
-
return cls(**_payload(raw, children))
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def shared_id(self) -> str | None:
|
|
38
|
-
return policy.shared_id(self.attrs)
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def text(self) -> str:
|
|
42
|
-
return " ".join(part for part in self.iter_text() if part)
|
|
43
|
-
|
|
44
|
-
def iter_text(self) -> Iterator[str]:
|
|
45
|
-
for child in self.content:
|
|
46
|
-
yield from child.iter_text()
|
|
47
|
-
|
|
48
|
-
def raw(self) -> Dict[str, Any]:
|
|
49
|
-
raw: Dict[str, Any] = {key.TYPE: self.kind}
|
|
50
|
-
raw.update(deepcopy(self.extra))
|
|
51
|
-
|
|
52
|
-
attrs = self.raw_attrs()
|
|
53
|
-
if attrs or key.ATTRS in self.present:
|
|
54
|
-
raw[key.ATTRS] = attrs
|
|
55
|
-
|
|
56
|
-
if self.content or key.CONTENT in self.present:
|
|
57
|
-
raw[key.CONTENT] = [child.raw() for child in self.content]
|
|
58
|
-
return raw
|
|
59
|
-
|
|
60
|
-
def raw_attrs(self) -> Dict[str, Any]:
|
|
61
|
-
attrs = deepcopy(self.attrs)
|
|
62
|
-
if self.id and not _has_any_identity(attrs):
|
|
63
|
-
attrs[key.ID] = self.id
|
|
64
|
-
return attrs
|
|
65
|
-
|
|
66
|
-
def with_text(self, value: str) -> "Node":
|
|
67
|
-
if not self.content:
|
|
68
|
-
return self
|
|
69
|
-
return replace(self, content=(Text(value=value),))
|
|
70
|
-
|
|
71
|
-
def with_attr(self, name: str, value: Any) -> "Node":
|
|
72
|
-
attrs = deepcopy(self.attrs)
|
|
73
|
-
attrs[name] = deepcopy(value)
|
|
74
|
-
return replace(self, attrs=attrs, present=self.present | {key.ATTRS})
|
|
75
|
-
|
|
76
|
-
def append(self, child: "Node") -> "Node":
|
|
77
|
-
return replace(self, content=self.content + (child,), present=self.present | {key.CONTENT})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@dataclass(frozen=True)
|
|
81
|
-
class Text(Node):
|
|
82
|
-
value: str = ""
|
|
83
|
-
marks: MarksTuple = ()
|
|
84
|
-
|
|
85
|
-
kind: ClassVar[str] = kind.TEXT
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def read(cls, raw: Mapping[str, Any], children: ContentTuple) -> "Text":
|
|
89
|
-
value = raw.get(key.TEXT, "")
|
|
90
|
-
marks = raw.get(key.MARKS, [])
|
|
91
|
-
return cls(
|
|
92
|
-
value=value if isinstance(value, str) else str(value),
|
|
93
|
-
marks=tuple(deepcopy(marks)) if isinstance(marks, list) else (),
|
|
94
|
-
**_payload(raw, children, extra_keys={key.TEXT, key.MARKS}),
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def text(self) -> str:
|
|
99
|
-
return self.value.strip()
|
|
100
|
-
|
|
101
|
-
def iter_text(self) -> Iterator[str]:
|
|
102
|
-
if self.text:
|
|
103
|
-
yield self.text
|
|
104
|
-
|
|
105
|
-
def raw(self) -> Dict[str, Any]:
|
|
106
|
-
raw = super().raw()
|
|
107
|
-
if self.value or key.TEXT in self.present:
|
|
108
|
-
raw[key.TEXT] = self.value
|
|
109
|
-
if self.marks or key.MARKS in self.present:
|
|
110
|
-
raw[key.MARKS] = [deepcopy(mark) for mark in self.marks]
|
|
111
|
-
return raw
|
|
112
|
-
|
|
113
|
-
def with_text(self, value: str) -> "Text":
|
|
114
|
-
return replace(self, value=value, present=self.present | {key.TEXT})
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@dataclass(frozen=True)
|
|
118
|
-
class Doc(Node):
|
|
119
|
-
kind: ClassVar[str] = kind.DOC
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
@dataclass(frozen=True)
|
|
123
|
-
class Paragraph(Node):
|
|
124
|
-
kind: ClassVar[str] = kind.PARAGRAPH
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@dataclass(frozen=True)
|
|
128
|
-
class Heading(Node):
|
|
129
|
-
level: int = 1
|
|
130
|
-
|
|
131
|
-
kind: ClassVar[str] = kind.HEADING
|
|
132
|
-
|
|
133
|
-
@classmethod
|
|
134
|
-
def read(cls, raw: Mapping[str, Any], children: ContentTuple) -> "Heading":
|
|
135
|
-
return cls(level=_heading_level(raw), **_payload(raw, children))
|
|
136
|
-
|
|
137
|
-
def raw_attrs(self) -> Dict[str, Any]:
|
|
138
|
-
attrs = super().raw_attrs()
|
|
139
|
-
if key.LEVEL in attrs or key.ATTRS not in self.present:
|
|
140
|
-
attrs.setdefault(key.LEVEL, self.level)
|
|
141
|
-
return attrs
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@dataclass(frozen=True)
|
|
145
|
-
class TaskItem(Node):
|
|
146
|
-
task_item_id: str = ""
|
|
147
|
-
is_completed: bool = False
|
|
148
|
-
local_task_item_id: str = ""
|
|
149
|
-
canonical_task_item_id: str = ""
|
|
150
|
-
is_linked_copy: bool = False
|
|
151
|
-
|
|
152
|
-
kind: ClassVar[str] = kind.TASK_ITEM
|
|
153
|
-
|
|
154
|
-
@classmethod
|
|
155
|
-
def read(cls, raw: Mapping[str, Any], children: ContentTuple) -> "TaskItem":
|
|
156
|
-
completed = _task_completion(raw)
|
|
157
|
-
local_id = policy.content_id(raw.get(key.ATTRS, {}))
|
|
158
|
-
canonical_id = _task_canonical_id(raw, local_id)
|
|
159
|
-
return cls(
|
|
160
|
-
task_item_id=canonical_id,
|
|
161
|
-
is_completed=bool(completed),
|
|
162
|
-
local_task_item_id=local_id,
|
|
163
|
-
canonical_task_item_id=canonical_id,
|
|
164
|
-
is_linked_copy=bool(local_id and canonical_id and local_id != canonical_id),
|
|
165
|
-
**_payload(raw, children),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
def __post_init__(self) -> None:
|
|
169
|
-
super().__post_init__()
|
|
170
|
-
if not self.local_task_item_id and self.task_item_id:
|
|
171
|
-
object.__setattr__(self, "local_task_item_id", self.task_item_id)
|
|
172
|
-
if not self.canonical_task_item_id and self.task_item_id:
|
|
173
|
-
object.__setattr__(self, "canonical_task_item_id", self.task_item_id)
|
|
174
|
-
if (
|
|
175
|
-
not self.is_linked_copy
|
|
176
|
-
and self.local_task_item_id
|
|
177
|
-
and self.canonical_task_item_id
|
|
178
|
-
and self.local_task_item_id != self.canonical_task_item_id
|
|
179
|
-
):
|
|
180
|
-
object.__setattr__(self, "is_linked_copy", True)
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def shared_id(self) -> str | None:
|
|
184
|
-
return policy.shared_id(self.attrs)
|
|
185
|
-
|
|
186
|
-
def raw_attrs(self) -> Dict[str, Any]:
|
|
187
|
-
attrs = super().raw_attrs()
|
|
188
|
-
if self.local_task_item_id and not _has_any_identity(attrs):
|
|
189
|
-
attrs[key.ID] = self.local_task_item_id
|
|
190
|
-
if self.canonical_task_item_id and (
|
|
191
|
-
key.TASK_CANONICAL_ID in attrs
|
|
192
|
-
or self.canonical_task_item_id != self.local_task_item_id
|
|
193
|
-
):
|
|
194
|
-
attrs.setdefault(key.TASK_CANONICAL_ID, self.canonical_task_item_id)
|
|
195
|
-
if key.CHECKED in attrs or (
|
|
196
|
-
key.ATTRS not in self.present and key.STATUS not in self.extra
|
|
197
|
-
):
|
|
198
|
-
attrs.setdefault(key.CHECKED, self.is_completed)
|
|
199
|
-
return attrs
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
@dataclass(frozen=True)
|
|
203
|
-
class ListItem(Node):
|
|
204
|
-
kind: ClassVar[str] = kind.LIST_ITEM
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@dataclass(frozen=True)
|
|
208
|
-
class CodeBlock(Node):
|
|
209
|
-
kind: ClassVar[str] = kind.CODE_BLOCK
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@dataclass(frozen=True)
|
|
213
|
-
class TaskList(Node):
|
|
214
|
-
kind: ClassVar[str] = kind.TASK_LIST
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@dataclass(frozen=True)
|
|
218
|
-
class BulletList(Node):
|
|
219
|
-
kind: ClassVar[str] = kind.BULLET_LIST
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@dataclass(frozen=True)
|
|
223
|
-
class OrderedList(Node):
|
|
224
|
-
kind: ClassVar[str] = kind.ORDERED_LIST
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@dataclass(frozen=True)
|
|
228
|
-
class Blockquote(Node):
|
|
229
|
-
kind: ClassVar[str] = kind.BLOCKQUOTE
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@dataclass(frozen=True)
|
|
233
|
-
class TableCell(Node):
|
|
234
|
-
kind: ClassVar[str] = kind.TABLE_CELL
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
@dataclass(frozen=True)
|
|
238
|
-
class Unknown(Node):
|
|
239
|
-
raw_kind: str = ""
|
|
240
|
-
|
|
241
|
-
@classmethod
|
|
242
|
-
def read(cls, raw: Mapping[str, Any], children: ContentTuple) -> "Unknown":
|
|
243
|
-
return cls(raw_kind=str(raw.get(key.TYPE, "")), **_payload(raw, children))
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def kind(self) -> str:
|
|
247
|
-
return self.raw_kind
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
class Registry:
|
|
251
|
-
"""Map raw TipTap kinds to typed node classes."""
|
|
252
|
-
|
|
253
|
-
def __init__(self) -> None:
|
|
254
|
-
self._classes: dict[str, type[Node]] = {}
|
|
255
|
-
|
|
256
|
-
def register(self, node_class: type[Node]) -> type[Node]:
|
|
257
|
-
self._classes[node_class.kind] = node_class
|
|
258
|
-
return node_class
|
|
259
|
-
|
|
260
|
-
def read(self, raw: Mapping[str, Any], children: ContentTuple) -> Node:
|
|
261
|
-
node_kind = str(raw.get(key.TYPE, ""))
|
|
262
|
-
node_class = self._classes.get(node_kind, Unknown)
|
|
263
|
-
return node_class.read(raw, children)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
registry = Registry()
|
|
267
|
-
for _node_class in (
|
|
12
|
+
from .base import ContentTuple, MarksTuple, Node, NodeT, Text
|
|
13
|
+
from .nodes import (
|
|
14
|
+
Blockquote,
|
|
15
|
+
BulletList,
|
|
16
|
+
CodeBlock,
|
|
268
17
|
Doc,
|
|
269
|
-
Text,
|
|
270
|
-
Paragraph,
|
|
271
18
|
Heading,
|
|
272
|
-
TaskItem,
|
|
273
19
|
ListItem,
|
|
274
|
-
CodeBlock,
|
|
275
|
-
TaskList,
|
|
276
|
-
BulletList,
|
|
277
20
|
OrderedList,
|
|
278
|
-
|
|
21
|
+
Paragraph,
|
|
279
22
|
TableCell,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def _task_canonical_id(raw: Mapping[str, Any], fallback: str) -> str:
|
|
309
|
-
attrs = raw.get(key.ATTRS, {})
|
|
310
|
-
if not isinstance(attrs, dict):
|
|
311
|
-
return fallback
|
|
312
|
-
|
|
313
|
-
canonical_id = attrs.get(key.TASK_CANONICAL_ID)
|
|
314
|
-
if isinstance(canonical_id, str):
|
|
315
|
-
stripped = canonical_id.strip()
|
|
316
|
-
if stripped:
|
|
317
|
-
return stripped
|
|
318
|
-
if canonical_id is not None:
|
|
319
|
-
return str(canonical_id)
|
|
320
|
-
return fallback
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def _task_completion(raw: Mapping[str, Any]) -> bool | None:
|
|
324
|
-
status = raw.get(key.STATUS)
|
|
325
|
-
if isinstance(status, str) and status.lower() in {"done", "pending"}:
|
|
326
|
-
return status.lower() == "done"
|
|
327
|
-
|
|
328
|
-
attrs = raw.get(key.ATTRS, {})
|
|
329
|
-
if not isinstance(attrs, dict):
|
|
330
|
-
attrs = {}
|
|
331
|
-
|
|
332
|
-
status = attrs.get(key.STATUS)
|
|
333
|
-
if isinstance(status, str) and status.lower() in {"done", "pending"}:
|
|
334
|
-
return status.lower() == "done"
|
|
335
|
-
|
|
336
|
-
checked = attrs.get(key.CHECKED)
|
|
337
|
-
if isinstance(checked, bool):
|
|
338
|
-
return checked
|
|
339
|
-
if isinstance(checked, str) and checked.strip().lower() in {"true", "false"}:
|
|
340
|
-
return checked.strip().lower() == "true"
|
|
341
|
-
return None
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _has_any_identity(attrs: Dict[str, Any]) -> bool:
|
|
345
|
-
return key.ID in attrs or key.TIPTAP_ID in attrs
|
|
23
|
+
TaskItem,
|
|
24
|
+
TaskList,
|
|
25
|
+
Unknown,
|
|
26
|
+
)
|
|
27
|
+
from .registry import Registry, registry
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Blockquote",
|
|
31
|
+
"BulletList",
|
|
32
|
+
"CodeBlock",
|
|
33
|
+
"ContentTuple",
|
|
34
|
+
"Doc",
|
|
35
|
+
"Heading",
|
|
36
|
+
"ListItem",
|
|
37
|
+
"MarksTuple",
|
|
38
|
+
"Node",
|
|
39
|
+
"NodeT",
|
|
40
|
+
"OrderedList",
|
|
41
|
+
"Paragraph",
|
|
42
|
+
"Registry",
|
|
43
|
+
"TableCell",
|
|
44
|
+
"TaskItem",
|
|
45
|
+
"TaskList",
|
|
46
|
+
"Text",
|
|
47
|
+
"Unknown",
|
|
48
|
+
"registry",
|
|
49
|
+
]
|