tiptap-python-utils 0.1.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.
Files changed (42) hide show
  1. tiptap_python_utils-0.1.0/LICENSE +21 -0
  2. tiptap_python_utils-0.1.0/MANIFEST.in +5 -0
  3. tiptap_python_utils-0.1.0/PKG-INFO +176 -0
  4. tiptap_python_utils-0.1.0/README.md +126 -0
  5. tiptap_python_utils-0.1.0/pyproject.toml +53 -0
  6. tiptap_python_utils-0.1.0/setup.cfg +4 -0
  7. tiptap_python_utils-0.1.0/src/tiptap_python_utils/__init__.py +85 -0
  8. tiptap_python_utils-0.1.0/src/tiptap_python_utils/codec/__init__.py +29 -0
  9. tiptap_python_utils-0.1.0/src/tiptap_python_utils/codec/json.py +116 -0
  10. tiptap_python_utils-0.1.0/src/tiptap_python_utils/content.py +174 -0
  11. tiptap_python_utils-0.1.0/src/tiptap_python_utils/contract/__init__.py +5 -0
  12. tiptap_python_utils-0.1.0/src/tiptap_python_utils/contract/key.py +14 -0
  13. tiptap_python_utils-0.1.0/src/tiptap_python_utils/contract/kind.py +14 -0
  14. tiptap_python_utils-0.1.0/src/tiptap_python_utils/contract/policy.py +52 -0
  15. tiptap_python_utils-0.1.0/src/tiptap_python_utils/edit/__init__.py +12 -0
  16. tiptap_python_utils-0.1.0/src/tiptap_python_utils/edit/commands.py +163 -0
  17. tiptap_python_utils-0.1.0/src/tiptap_python_utils/exceptions.py +5 -0
  18. tiptap_python_utils-0.1.0/src/tiptap_python_utils/model/__init__.py +345 -0
  19. tiptap_python_utils-0.1.0/src/tiptap_python_utils/py.typed +1 -0
  20. tiptap_python_utils-0.1.0/src/tiptap_python_utils/select/__init__.py +5 -0
  21. tiptap_python_utils-0.1.0/src/tiptap_python_utils/select/selection.py +72 -0
  22. tiptap_python_utils-0.1.0/src/tiptap_python_utils/shared/__init__.py +23 -0
  23. tiptap_python_utils-0.1.0/src/tiptap_python_utils/shared/service.py +147 -0
  24. tiptap_python_utils-0.1.0/src/tiptap_python_utils/tasks/__init__.py +5 -0
  25. tiptap_python_utils-0.1.0/src/tiptap_python_utils/tasks/query.py +18 -0
  26. tiptap_python_utils-0.1.0/src/tiptap_python_utils/text/__init__.py +5 -0
  27. tiptap_python_utils-0.1.0/src/tiptap_python_utils/text/extract.py +113 -0
  28. tiptap_python_utils-0.1.0/src/tiptap_python_utils/tree/__init__.py +5 -0
  29. tiptap_python_utils-0.1.0/src/tiptap_python_utils/tree/path.py +36 -0
  30. tiptap_python_utils-0.1.0/src/tiptap_python_utils/types.py +7 -0
  31. tiptap_python_utils-0.1.0/src/tiptap_python_utils/walk/__init__.py +5 -0
  32. tiptap_python_utils-0.1.0/src/tiptap_python_utils/walk/traversal.py +88 -0
  33. tiptap_python_utils-0.1.0/src/tiptap_python_utils.egg-info/PKG-INFO +176 -0
  34. tiptap_python_utils-0.1.0/src/tiptap_python_utils.egg-info/SOURCES.txt +40 -0
  35. tiptap_python_utils-0.1.0/src/tiptap_python_utils.egg-info/dependency_links.txt +1 -0
  36. tiptap_python_utils-0.1.0/src/tiptap_python_utils.egg-info/requires.txt +5 -0
  37. tiptap_python_utils-0.1.0/src/tiptap_python_utils.egg-info/top_level.txt +1 -0
  38. tiptap_python_utils-0.1.0/tests/test_content.py +283 -0
  39. tiptap_python_utils-0.1.0/tests/test_extract.py +44 -0
  40. tiptap_python_utils-0.1.0/tests/test_filter.py +21 -0
  41. tiptap_python_utils-0.1.0/tests/test_mutations.py +338 -0
  42. tiptap_python_utils-0.1.0/tests/test_traverser.py +63 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tiptap_python_utils contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ include src/tiptap_python_utils/py.typed
5
+ recursive-include tests *.py
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiptap_python_utils
3
+ Version: 0.1.0
4
+ Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
5
+ Author: tiptap_python_utils contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 tiptap_python_utils contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/tugkanpilka/tiptap-python-utils
29
+ Project-URL: Repository, https://github.com/tugkanpilka/tiptap-python-utils
30
+ Project-URL: Issues, https://github.com/tugkanpilka/tiptap-python-utils/issues
31
+ Project-URL: Changelog, https://github.com/tugkanpilka/tiptap-python-utils/blob/main/CHANGELOG.md
32
+ Keywords: tiptap,prosemirror,json,ast,editor
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.9
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Provides-Extra: dev
46
+ Requires-Dist: build>=1.2; extra == "dev"
47
+ Requires-Dist: pytest>=8; extra == "dev"
48
+ Requires-Dist: twine>=5; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ # tiptap_python_utils
52
+
53
+ Python utilities for TipTap JSON content.
54
+
55
+ `tiptap_python_utils` parses TipTap documents into typed Python nodes, preserves
56
+ unknown/custom nodes for lossless round trips, and provides small helpers for
57
+ traversal, immutable edits, visible text extraction, task queries, and shared
58
+ node synchronization.
59
+
60
+ The package has no runtime dependencies.
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install tiptap_python_utils
66
+ ```
67
+
68
+ ## Quick Start
69
+
70
+ ```python
71
+ from tiptap_python_utils import Content, Paragraph, Text, kind
72
+
73
+ raw = {
74
+ "type": "doc",
75
+ "content": [
76
+ {
77
+ "type": "paragraph",
78
+ "attrs": {"id": "p1"},
79
+ "content": [{"type": "text", "text": "Old"}],
80
+ }
81
+ ],
82
+ }
83
+
84
+ updated = Content.require(raw).where_id("p1").text("New").dump()
85
+ ```
86
+
87
+ ## Typed Nodes
88
+
89
+ Build typed nodes directly and serialize them back to TipTap-compatible JSON:
90
+
91
+ ```python
92
+ node = Paragraph(id="p1", content=(Text(value="Hello"),))
93
+ doc = Content.wrap(node.raw())
94
+ ```
95
+
96
+ Unknown/custom node types are preserved as `Unknown` nodes and round-trip without
97
+ dropping extra fields.
98
+
99
+ ## Selection And Editing
100
+
101
+ Select nodes by id or TipTap kind:
102
+
103
+ ```python
104
+ updated = Content.require(raw).of(kind.PARAGRAPH).attr("color", "blue").dump()
105
+ ```
106
+
107
+ Selection methods return updated immutable content:
108
+
109
+ ```python
110
+ updated = (
111
+ Content.require(raw)
112
+ .where_id("p1")
113
+ .text("Updated")
114
+ .attr("data-state", "reviewed")
115
+ .dump()
116
+ )
117
+ ```
118
+
119
+ ## Text Extraction
120
+
121
+ Extract visible text or contextual slices:
122
+
123
+ ```python
124
+ from tiptap_python_utils import Content, text_slices, visible_text, word_count
125
+
126
+ content = Content.require(raw)
127
+
128
+ plain_text = visible_text(content)
129
+ count = word_count(content)
130
+ slices = text_slices(content, context=True)
131
+ ```
132
+
133
+ ## Tasks
134
+
135
+ ```python
136
+ from tiptap_python_utils import Content, has_open_tasks, open_tasks
137
+
138
+ content = Content.require(raw)
139
+
140
+ pending = has_open_tasks(content)
141
+ items = open_tasks(content)
142
+ ```
143
+
144
+ ## Public API
145
+
146
+ Common imports are available from the package root:
147
+
148
+ ```python
149
+ from tiptap_python_utils import (
150
+ Content,
151
+ Paragraph,
152
+ TaskItem,
153
+ Text,
154
+ append_node,
155
+ has_open_tasks,
156
+ kind,
157
+ replace_node,
158
+ shared_families,
159
+ sync_shared,
160
+ text_slices,
161
+ )
162
+ ```
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ python -m pip install -e ".[dev]"
168
+ pytest -q
169
+ ```
170
+
171
+ Build and check a release artifact:
172
+
173
+ ```bash
174
+ python -m build
175
+ python -m twine check dist/*
176
+ ```
@@ -0,0 +1,126 @@
1
+ # tiptap_python_utils
2
+
3
+ Python utilities for TipTap JSON content.
4
+
5
+ `tiptap_python_utils` parses TipTap documents into typed Python nodes, preserves
6
+ unknown/custom nodes for lossless round trips, and provides small helpers for
7
+ traversal, immutable edits, visible text extraction, task queries, and shared
8
+ node synchronization.
9
+
10
+ The package has no runtime dependencies.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install tiptap_python_utils
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ from tiptap_python_utils import Content, Paragraph, Text, kind
22
+
23
+ raw = {
24
+ "type": "doc",
25
+ "content": [
26
+ {
27
+ "type": "paragraph",
28
+ "attrs": {"id": "p1"},
29
+ "content": [{"type": "text", "text": "Old"}],
30
+ }
31
+ ],
32
+ }
33
+
34
+ updated = Content.require(raw).where_id("p1").text("New").dump()
35
+ ```
36
+
37
+ ## Typed Nodes
38
+
39
+ Build typed nodes directly and serialize them back to TipTap-compatible JSON:
40
+
41
+ ```python
42
+ node = Paragraph(id="p1", content=(Text(value="Hello"),))
43
+ doc = Content.wrap(node.raw())
44
+ ```
45
+
46
+ Unknown/custom node types are preserved as `Unknown` nodes and round-trip without
47
+ dropping extra fields.
48
+
49
+ ## Selection And Editing
50
+
51
+ Select nodes by id or TipTap kind:
52
+
53
+ ```python
54
+ updated = Content.require(raw).of(kind.PARAGRAPH).attr("color", "blue").dump()
55
+ ```
56
+
57
+ Selection methods return updated immutable content:
58
+
59
+ ```python
60
+ updated = (
61
+ Content.require(raw)
62
+ .where_id("p1")
63
+ .text("Updated")
64
+ .attr("data-state", "reviewed")
65
+ .dump()
66
+ )
67
+ ```
68
+
69
+ ## Text Extraction
70
+
71
+ Extract visible text or contextual slices:
72
+
73
+ ```python
74
+ from tiptap_python_utils import Content, text_slices, visible_text, word_count
75
+
76
+ content = Content.require(raw)
77
+
78
+ plain_text = visible_text(content)
79
+ count = word_count(content)
80
+ slices = text_slices(content, context=True)
81
+ ```
82
+
83
+ ## Tasks
84
+
85
+ ```python
86
+ from tiptap_python_utils import Content, has_open_tasks, open_tasks
87
+
88
+ content = Content.require(raw)
89
+
90
+ pending = has_open_tasks(content)
91
+ items = open_tasks(content)
92
+ ```
93
+
94
+ ## Public API
95
+
96
+ Common imports are available from the package root:
97
+
98
+ ```python
99
+ from tiptap_python_utils import (
100
+ Content,
101
+ Paragraph,
102
+ TaskItem,
103
+ Text,
104
+ append_node,
105
+ has_open_tasks,
106
+ kind,
107
+ replace_node,
108
+ shared_families,
109
+ sync_shared,
110
+ text_slices,
111
+ )
112
+ ```
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ python -m pip install -e ".[dev]"
118
+ pytest -q
119
+ ```
120
+
121
+ Build and check a release artifact:
122
+
123
+ ```bash
124
+ python -m build
125
+ python -m twine check dist/*
126
+ ```
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tiptap_python_utils"
7
+ version = "0.1.0"
8
+ description = "Python utilities for parsing, traversing, editing, and serializing TipTap JSON content."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "tiptap_python_utils contributors" }
14
+ ]
15
+ keywords = ["tiptap", "prosemirror", "json", "ast", "editor"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/tugkanpilka/tiptap-python-utils"
31
+ Repository = "https://github.com/tugkanpilka/tiptap-python-utils"
32
+ Issues = "https://github.com/tugkanpilka/tiptap-python-utils/issues"
33
+ Changelog = "https://github.com/tugkanpilka/tiptap-python-utils/blob/main/CHANGELOG.md"
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "build>=1.2",
38
+ "pytest>=8",
39
+ "twine>=5",
40
+ ]
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.setuptools.package-data]
46
+ tiptap_python_utils = ["py.typed"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ pythonpath = ["src"]
51
+ markers = [
52
+ "unit: unit tests",
53
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ """Public API for TipTap JSON utilities."""
2
+
3
+ from .contract import key, kind
4
+ from .content import Content
5
+ from .edit import append_node, replace_node
6
+ from .exceptions import TiptapValidationError
7
+ from .model import (
8
+ Blockquote,
9
+ BulletList,
10
+ CodeBlock,
11
+ Doc,
12
+ Heading,
13
+ ListItem,
14
+ Node,
15
+ OrderedList,
16
+ Paragraph,
17
+ TableCell,
18
+ TaskItem,
19
+ TaskList,
20
+ Text,
21
+ Unknown,
22
+ registry,
23
+ )
24
+ from .contract.policy import content_id, is_parseable, node_id, tiptap_id
25
+ from .select import Selection
26
+ from .shared import (
27
+ fingerprint_shared,
28
+ has_shared,
29
+ new_shared_id,
30
+ shared_id,
31
+ shared_families,
32
+ stamp_shared,
33
+ sync_shared,
34
+ )
35
+ from .tasks import has_open_tasks, open_tasks, syncable_tasks
36
+ from .text import NodeText, text_slices, visible_text, word_count
37
+ from .walk import Ref, Walker
38
+
39
+ EMPTY_DOCUMENT_CONTENT = '{"type":"doc","content":[]}'
40
+
41
+ __all__ = [
42
+ "Blockquote",
43
+ "BulletList",
44
+ "CodeBlock",
45
+ "Content",
46
+ "Doc",
47
+ "EMPTY_DOCUMENT_CONTENT",
48
+ "Heading",
49
+ "ListItem",
50
+ "Node",
51
+ "NodeText",
52
+ "OrderedList",
53
+ "Paragraph",
54
+ "Ref",
55
+ "Selection",
56
+ "TableCell",
57
+ "TaskItem",
58
+ "TaskList",
59
+ "Text",
60
+ "TiptapValidationError",
61
+ "Unknown",
62
+ "Walker",
63
+ "append_node",
64
+ "content_id",
65
+ "fingerprint_shared",
66
+ "has_open_tasks",
67
+ "has_shared",
68
+ "is_parseable",
69
+ "key",
70
+ "kind",
71
+ "new_shared_id",
72
+ "node_id",
73
+ "open_tasks",
74
+ "registry",
75
+ "replace_node",
76
+ "shared_families",
77
+ "shared_id",
78
+ "stamp_shared",
79
+ "sync_shared",
80
+ "syncable_tasks",
81
+ "text_slices",
82
+ "tiptap_id",
83
+ "visible_text",
84
+ "word_count",
85
+ ]
@@ -0,0 +1,29 @@
1
+ """Raw JSON codec exports."""
2
+
3
+ from .json import (
4
+ dump,
5
+ dumps,
6
+ normalize_text,
7
+ parse_raw,
8
+ raw_node_id,
9
+ raw_text,
10
+ read_children,
11
+ read_doc,
12
+ read_node,
13
+ read_node_input,
14
+ require_object,
15
+ )
16
+
17
+ __all__ = [
18
+ "dump",
19
+ "dumps",
20
+ "normalize_text",
21
+ "parse_raw",
22
+ "raw_node_id",
23
+ "raw_text",
24
+ "read_children",
25
+ "read_doc",
26
+ "read_node",
27
+ "read_node_input",
28
+ "require_object",
29
+ ]
@@ -0,0 +1,116 @@
1
+ """TipTap raw JSON codec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from copy import deepcopy
7
+ from typing import Any, Dict, Mapping, Optional
8
+
9
+ from ..exceptions import TiptapValidationError
10
+ from ..types import DocumentContent
11
+
12
+ from ..contract import key, kind, policy
13
+ from ..model import ContentTuple, Doc, Node, registry
14
+
15
+
16
+ def parse_raw(raw: Optional[DocumentContent]) -> Optional[Dict[str, Any]]:
17
+ """Leniently parse a raw document payload."""
18
+ if isinstance(raw, str):
19
+ try:
20
+ parsed = json.loads(raw)
21
+ except json.JSONDecodeError:
22
+ return None
23
+ else:
24
+ parsed = raw
25
+
26
+ return deepcopy(parsed) if isinstance(parsed, dict) else None
27
+
28
+
29
+ def require_object(
30
+ content: str | Mapping[str, Any],
31
+ *,
32
+ label: str = "content",
33
+ ) -> Dict[str, Any]:
34
+ """Strictly parse a JSON object payload."""
35
+ if isinstance(content, str):
36
+ try:
37
+ parsed = json.loads(content)
38
+ except json.JSONDecodeError as exc:
39
+ raise TiptapValidationError(f"{label} is not valid JSON") from exc
40
+ else:
41
+ parsed = deepcopy(dict(content))
42
+
43
+ if not isinstance(parsed, dict):
44
+ raise TiptapValidationError(f"{label} must be a JSON object")
45
+ return parsed
46
+
47
+
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
+ def raw_node_id(node: Mapping[str, Any]) -> str:
89
+ return policy.content_id(node.get(key.ATTRS, {}))
90
+
91
+
92
+ def raw_text(node: Mapping[str, Any]) -> str:
93
+ return " ".join(_iter_text(node))
94
+
95
+
96
+ def normalize_text(value: str) -> str:
97
+ return " ".join(value.split())
98
+
99
+
100
+ def _iter_text(node: Mapping[str, Any]):
101
+ if not isinstance(node, dict):
102
+ return
103
+
104
+ if node.get(key.TYPE) == kind.TEXT:
105
+ text = str(node.get(key.TEXT, "")).strip()
106
+ if text:
107
+ yield text
108
+ return
109
+
110
+ content = node.get(key.CONTENT, [])
111
+ if not isinstance(content, list):
112
+ return
113
+
114
+ for child in content:
115
+ if isinstance(child, dict):
116
+ yield from _iter_text(child)