tiptap-python-utils 0.5.0__tar.gz → 0.6.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 (55) hide show
  1. {tiptap_python_utils-0.5.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.6.0}/PKG-INFO +47 -58
  2. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/README.md +45 -56
  3. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/pyproject.toml +2 -2
  4. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/__init__.py +2 -0
  5. tiptap_python_utils-0.6.0/src/tiptap_python_utils/task.py +55 -0
  6. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0/src/tiptap_python_utils.egg-info}/PKG-INFO +47 -58
  7. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils.egg-info/SOURCES.txt +2 -0
  8. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_compat_imports.py +1 -0
  9. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_public_api.py +1 -0
  10. tiptap_python_utils-0.6.0/tests/test_task_namespace.py +90 -0
  11. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/LICENSE +0 -0
  12. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/MANIFEST.in +0 -0
  13. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/setup.cfg +0 -0
  14. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/codec/__init__.py +0 -0
  15. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/codec/raw.py +0 -0
  16. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/codec/reader.py +0 -0
  17. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/codec/writer.py +0 -0
  18. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/content.py +0 -0
  19. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/contract/__init__.py +0 -0
  20. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/contract/key.py +0 -0
  21. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/contract/kind.py +0 -0
  22. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/contract/policy.py +0 -0
  23. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/exceptions.py +0 -0
  24. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/identity.py +0 -0
  25. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/model/__init__.py +0 -0
  26. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/model/base.py +0 -0
  27. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/model/nodes.py +0 -0
  28. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/model/payload.py +0 -0
  29. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/model/registry.py +0 -0
  30. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/py.typed +0 -0
  31. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/select/__init__.py +0 -0
  32. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/select/selection.py +0 -0
  33. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/shared/__init__.py +0 -0
  34. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/shared/families.py +0 -0
  35. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/shared/fingerprint.py +0 -0
  36. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/shared/identity.py +0 -0
  37. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/tasks/__init__.py +0 -0
  38. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/tasks/query.py +0 -0
  39. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/text/__init__.py +0 -0
  40. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/text/extract.py +0 -0
  41. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/tree/__init__.py +0 -0
  42. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/tree/path.py +0 -0
  43. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/types.py +0 -0
  44. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/walk/__init__.py +0 -0
  45. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils/walk/traversal.py +0 -0
  46. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils.egg-info/dependency_links.txt +0 -0
  47. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils.egg-info/requires.txt +0 -0
  48. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/src/tiptap_python_utils.egg-info/top_level.txt +0 -0
  49. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_codec_raw.py +0 -0
  50. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_content.py +0 -0
  51. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_extract.py +0 -0
  52. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_filter.py +0 -0
  53. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_generic_helpers.py +0 -0
  54. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_mutations.py +0 -0
  55. {tiptap_python_utils-0.5.0 → tiptap_python_utils-0.6.0}/tests/test_traverser.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tiptap_python_utils
3
- Version: 0.5.0
4
- Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
3
+ Version: 0.6.0
4
+ Summary: Pure-Python utilities for processing TipTap JSON on the server side. Parse, traverse, edit, and serialize TipTap documents — no JavaScript bridge required. Zero runtime dependencies, Python 3.9+.
5
5
  Author: tiptap_python_utils contributors
6
6
  License: MIT License
7
7
 
@@ -56,23 +56,10 @@ Dynamic: license-file
56
56
  [![CI](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
58
58
 
59
- Python utilities for [TipTap](https://tiptap.dev) JSON content.
60
-
61
- `tiptap_python_utils` parses TipTap documents into typed, immutable Python
62
- nodes, preserves unknown/custom nodes for lossless round trips, and provides
63
- small helpers for traversal, immutable edits, visible text extraction, task
64
- queries, and shared-node synchronization.
65
-
66
- - **Zero runtime dependencies.** Standard library only.
67
- - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
68
- - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
69
- - **Immutable AST.** All mutations return new instances via a fluent selection API.
70
-
71
- ## Install
72
-
73
- ```bash
74
- pip install tiptap_python_utils
75
- ```
59
+ TipTap is a JavaScript editor. If your backend is Python and you need to
60
+ process TipTap JSON — extract text, query tasks, sync shared nodes — this
61
+ library does it in pure Python with zero dependencies. No JS bridge, no
62
+ Node.js subprocess.
76
63
 
77
64
  ## Quick Start
78
65
 
@@ -94,8 +81,24 @@ raw = {
94
81
  updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
95
82
  ```
96
83
 
84
+ ## Features
85
+
86
+ - **Zero runtime dependencies.** Standard library only.
87
+ - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
88
+ - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
89
+ - **Immutable AST.** All mutations return new instances via a fluent selection API.
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ pip install tiptap_python_utils
95
+ ```
96
+
97
97
  ## Three Ways to Load a Document
98
98
 
99
+ Pick a constructor by how much you trust the input — lenient, strict, or
100
+ auto-wrapping a bare node into a `doc`.
101
+
99
102
  | Constructor | When to use | On invalid input |
100
103
  |---|---|---|
101
104
  | `Content.parse(raw)` | Lenient — `raw` may be `None`, a string, or a dict | Returns a `Content` with `root=None` |
@@ -104,7 +107,8 @@ updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
104
107
 
105
108
  ## Lossless Round Trip
106
109
 
107
- Parsing never silently drops fields. Two mechanisms preserve information:
110
+ Parsing never silently drops fields custom nodes and unknown keys survive a
111
+ parse-then-serialize cycle byte-for-byte. Two mechanisms preserve information:
108
112
 
109
113
  - `Node.extra` stores top-level keys that aren't part of the known schema
110
114
  (e.g. custom node attributes, vendor-specific keys).
@@ -125,7 +129,8 @@ assert Content.require(raw).to_dict() == raw # byte-for-byte
125
129
 
126
130
  ## Typed Nodes
127
131
 
128
- Build typed nodes directly and serialize them back to TipTap-compatible JSON:
132
+ Build typed nodes directly in Python and serialize them back to
133
+ TipTap-compatible JSON.
129
134
 
130
135
  ```python
131
136
  from tiptap_python_utils import Content, Paragraph, Text
@@ -136,8 +141,8 @@ doc = Content.wrap(node.raw())
136
141
 
137
142
  ## Selection and Editing
138
143
 
139
- The fluent selection API is the single home for mutation. Selection methods
140
- return a new `Content`; the original is never mutated.
144
+ The fluent selection API is the single home for mutation: every method returns
145
+ a new `Content`, so the original is never mutated.
141
146
 
142
147
  ### Select by id or kind
143
148
 
@@ -206,6 +211,9 @@ content.replace_by_id("p1", {
206
211
 
207
212
  ## Text Extraction
208
213
 
214
+ Pull the visible plain text out of a document — useful for search indexing,
215
+ word counts, or previews.
216
+
209
217
  ```python
210
218
  from tiptap_python_utils import Content, text_slices, visible_text, word_count
211
219
 
@@ -218,6 +226,9 @@ slices = text_slices(content, context=True)
218
226
 
219
227
  ## Tasks
220
228
 
229
+ Query task lists in a document — find every task item or check whether any are
230
+ still open.
231
+
221
232
  ```python
222
233
  from tiptap_python_utils import Content, has_open_tasks, open_tasks
223
234
 
@@ -240,6 +251,9 @@ task.shared_id # sharedId attr, if any
240
251
 
241
252
  ## Shared-Node Synchronization
242
253
 
254
+ Keep copies of the same logical node (linked by `sharedId`) in sync — collect
255
+ canonical bodies, then rewrite every matching node from them.
256
+
243
257
  `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
244
258
  a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
245
259
  matching node in the document from those canonical bodies, preserving
@@ -278,17 +292,6 @@ Related helpers on `Content`:
278
292
  - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
279
293
  - `new_shared_id()` — mint a fresh `shared-…` identifier.
280
294
 
281
- ## Architecture (one paragraph)
282
-
283
- The package is layered: `contract` (key/kind/policy primitives) → `model`
284
- (immutable AST with a registry of node classes; unknown kinds round-trip via
285
- `Unknown`) → `codec` (raw I/O in `raw.py`, hydration in `reader.py`, dump in
286
- `writer.py`) → `walk` & `tree` (traversal + path-based replacement on the
287
- immutable tree) → `select` (fluent `Selection` — the single home for mutation)
288
- → `content` (public facade) → `text` / `tasks` / `shared` (user-facing
289
- workflows built on `Content`). All nodes are `@dataclass(frozen=True)`;
290
- mutations return new instances.
291
-
292
295
  ## Public API
293
296
 
294
297
  Common imports are available from the package root:
@@ -311,35 +314,21 @@ from tiptap_python_utils import (
311
314
  )
312
315
  ```
313
316
 
314
- ## Stability
315
-
316
- The project is pre-1.0; minor versions may include breaking changes. See
317
- [CHANGELOG.md](CHANGELOG.md) for what changed and when.
318
-
319
- ## Development
320
-
321
- ```bash
322
- python -m venv .venv
323
- . .venv/bin/activate
324
- python -m pip install -e ".[dev]"
325
- pytest -q
326
- ```
327
-
328
- Build and validate a release artifact:
329
-
330
- ```bash
331
- python -m build
332
- python -m twine check dist/*
333
- ```
334
-
335
317
  ## Contributing
336
318
 
337
319
  Issues and pull requests are welcome. Please read
338
- [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup and release checklist,
339
- and open an issue at
320
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup, architecture overview,
321
+ and release checklist, and open an issue at
340
322
  [github.com/tugkanpilka/tiptap-python-utils/issues](https://github.com/tugkanpilka/tiptap-python-utils/issues)
341
- before larger changes so we can align on the approach.
323
+ before opening a pull request so we can align on the approach.
342
324
 
343
325
  ## License
344
326
 
345
327
  MIT — see [LICENSE](LICENSE).
328
+
329
+ ## Stability
330
+
331
+ The project is pre-1.0; minor versions may include breaking changes. See
332
+ [CHANGELOG.md](CHANGELOG.md) for what changed and when.
333
+ </content>
334
+ </invoke>
@@ -5,23 +5,10 @@
5
5
  [![CI](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
 
8
- Python utilities for [TipTap](https://tiptap.dev) JSON content.
9
-
10
- `tiptap_python_utils` parses TipTap documents into typed, immutable Python
11
- nodes, preserves unknown/custom nodes for lossless round trips, and provides
12
- small helpers for traversal, immutable edits, visible text extraction, task
13
- queries, and shared-node synchronization.
14
-
15
- - **Zero runtime dependencies.** Standard library only.
16
- - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
17
- - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
18
- - **Immutable AST.** All mutations return new instances via a fluent selection API.
19
-
20
- ## Install
21
-
22
- ```bash
23
- pip install tiptap_python_utils
24
- ```
8
+ TipTap is a JavaScript editor. If your backend is Python and you need to
9
+ process TipTap JSON — extract text, query tasks, sync shared nodes — this
10
+ library does it in pure Python with zero dependencies. No JS bridge, no
11
+ Node.js subprocess.
25
12
 
26
13
  ## Quick Start
27
14
 
@@ -43,8 +30,24 @@ raw = {
43
30
  updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
44
31
  ```
45
32
 
33
+ ## Features
34
+
35
+ - **Zero runtime dependencies.** Standard library only.
36
+ - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
37
+ - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
38
+ - **Immutable AST.** All mutations return new instances via a fluent selection API.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install tiptap_python_utils
44
+ ```
45
+
46
46
  ## Three Ways to Load a Document
47
47
 
48
+ Pick a constructor by how much you trust the input — lenient, strict, or
49
+ auto-wrapping a bare node into a `doc`.
50
+
48
51
  | Constructor | When to use | On invalid input |
49
52
  |---|---|---|
50
53
  | `Content.parse(raw)` | Lenient — `raw` may be `None`, a string, or a dict | Returns a `Content` with `root=None` |
@@ -53,7 +56,8 @@ updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
53
56
 
54
57
  ## Lossless Round Trip
55
58
 
56
- Parsing never silently drops fields. Two mechanisms preserve information:
59
+ Parsing never silently drops fields custom nodes and unknown keys survive a
60
+ parse-then-serialize cycle byte-for-byte. Two mechanisms preserve information:
57
61
 
58
62
  - `Node.extra` stores top-level keys that aren't part of the known schema
59
63
  (e.g. custom node attributes, vendor-specific keys).
@@ -74,7 +78,8 @@ assert Content.require(raw).to_dict() == raw # byte-for-byte
74
78
 
75
79
  ## Typed Nodes
76
80
 
77
- Build typed nodes directly and serialize them back to TipTap-compatible JSON:
81
+ Build typed nodes directly in Python and serialize them back to
82
+ TipTap-compatible JSON.
78
83
 
79
84
  ```python
80
85
  from tiptap_python_utils import Content, Paragraph, Text
@@ -85,8 +90,8 @@ doc = Content.wrap(node.raw())
85
90
 
86
91
  ## Selection and Editing
87
92
 
88
- The fluent selection API is the single home for mutation. Selection methods
89
- return a new `Content`; the original is never mutated.
93
+ The fluent selection API is the single home for mutation: every method returns
94
+ a new `Content`, so the original is never mutated.
90
95
 
91
96
  ### Select by id or kind
92
97
 
@@ -155,6 +160,9 @@ content.replace_by_id("p1", {
155
160
 
156
161
  ## Text Extraction
157
162
 
163
+ Pull the visible plain text out of a document — useful for search indexing,
164
+ word counts, or previews.
165
+
158
166
  ```python
159
167
  from tiptap_python_utils import Content, text_slices, visible_text, word_count
160
168
 
@@ -167,6 +175,9 @@ slices = text_slices(content, context=True)
167
175
 
168
176
  ## Tasks
169
177
 
178
+ Query task lists in a document — find every task item or check whether any are
179
+ still open.
180
+
170
181
  ```python
171
182
  from tiptap_python_utils import Content, has_open_tasks, open_tasks
172
183
 
@@ -189,6 +200,9 @@ task.shared_id # sharedId attr, if any
189
200
 
190
201
  ## Shared-Node Synchronization
191
202
 
203
+ Keep copies of the same logical node (linked by `sharedId`) in sync — collect
204
+ canonical bodies, then rewrite every matching node from them.
205
+
192
206
  `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
193
207
  a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
194
208
  matching node in the document from those canonical bodies, preserving
@@ -227,17 +241,6 @@ Related helpers on `Content`:
227
241
  - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
228
242
  - `new_shared_id()` — mint a fresh `shared-…` identifier.
229
243
 
230
- ## Architecture (one paragraph)
231
-
232
- The package is layered: `contract` (key/kind/policy primitives) → `model`
233
- (immutable AST with a registry of node classes; unknown kinds round-trip via
234
- `Unknown`) → `codec` (raw I/O in `raw.py`, hydration in `reader.py`, dump in
235
- `writer.py`) → `walk` & `tree` (traversal + path-based replacement on the
236
- immutable tree) → `select` (fluent `Selection` — the single home for mutation)
237
- → `content` (public facade) → `text` / `tasks` / `shared` (user-facing
238
- workflows built on `Content`). All nodes are `@dataclass(frozen=True)`;
239
- mutations return new instances.
240
-
241
244
  ## Public API
242
245
 
243
246
  Common imports are available from the package root:
@@ -260,35 +263,21 @@ from tiptap_python_utils import (
260
263
  )
261
264
  ```
262
265
 
263
- ## Stability
264
-
265
- The project is pre-1.0; minor versions may include breaking changes. See
266
- [CHANGELOG.md](CHANGELOG.md) for what changed and when.
267
-
268
- ## Development
269
-
270
- ```bash
271
- python -m venv .venv
272
- . .venv/bin/activate
273
- python -m pip install -e ".[dev]"
274
- pytest -q
275
- ```
276
-
277
- Build and validate a release artifact:
278
-
279
- ```bash
280
- python -m build
281
- python -m twine check dist/*
282
- ```
283
-
284
266
  ## Contributing
285
267
 
286
268
  Issues and pull requests are welcome. Please read
287
- [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup and release checklist,
288
- and open an issue at
269
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup, architecture overview,
270
+ and release checklist, and open an issue at
289
271
  [github.com/tugkanpilka/tiptap-python-utils/issues](https://github.com/tugkanpilka/tiptap-python-utils/issues)
290
- before larger changes so we can align on the approach.
272
+ before opening a pull request so we can align on the approach.
291
273
 
292
274
  ## License
293
275
 
294
276
  MIT — see [LICENSE](LICENSE).
277
+
278
+ ## Stability
279
+
280
+ The project is pre-1.0; minor versions may include breaking changes. See
281
+ [CHANGELOG.md](CHANGELOG.md) for what changed and when.
282
+ </content>
283
+ </invoke>
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tiptap_python_utils"
7
- version = "0.5.0"
8
- description = "Python utilities for parsing, traversing, editing, and serializing TipTap JSON content."
7
+ version = "0.6.0"
8
+ description = "Pure-Python utilities for processing TipTap JSON on the server side. Parse, traverse, edit, and serialize TipTap documents — no JavaScript bridge required. Zero runtime dependencies, Python 3.9+."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { file = "LICENSE" }
@@ -1,5 +1,6 @@
1
1
  """Public API for TipTap JSON utilities."""
2
2
 
3
+ from . import task
3
4
  from .contract import key, kind
4
5
  from .content import Content
5
6
  from .exceptions import TiptapValidationError
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "open_tasks",
66
67
  "registry",
67
68
  "syncable_tasks",
69
+ "task",
68
70
  "text_slices",
69
71
  "tiptap_id",
70
72
  "visible_text",
@@ -0,0 +1,55 @@
1
+ """Pure, raw-dict helpers for TipTap ``taskList`` / ``taskItem`` shapes.
2
+
3
+ This module is the raw-dict counterpart to the typed ``tasks`` package:
4
+ ``tasks`` works on hydrated ``Content``/``TaskItem`` objects, while ``task``
5
+ deals only with plain TipTap JSON dicts and never imports the model layer.
6
+
7
+ Read as a namespace: ``task.create_list(...)``, ``task.is_list(...)``,
8
+ ``task.is_item(...)``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, List
14
+
15
+ from .contract import key, kind
16
+
17
+ __all__ = ["create_list", "is_item", "is_list"]
18
+
19
+
20
+ def create_list(items: List[Any]) -> dict:
21
+ """Wrap ``items`` in a ``taskList`` block.
22
+
23
+ By design this does **not** copy ``items`` — the same list object is
24
+ stored on the returned block's ``content`` (by reference). Callers may
25
+ keep appending to the original list afterwards and see the change
26
+ reflected here. Do not introduce ``deepcopy``/``list(...)``/``[*items]``
27
+ here: the shared-reference behaviour is part of the contract and is
28
+ pinned by ``tests/test_task_namespace.py``.
29
+ """
30
+ return {key.TYPE: kind.TASK_LIST, key.CONTENT: items}
31
+
32
+
33
+ def is_list(item: Any) -> bool:
34
+ """True iff ``item`` is a valid ``taskList`` dict.
35
+
36
+ Safe on missing keys (uses ``.get()``): all three conditions must hold —
37
+ ``item`` is a dict, its ``type`` is ``taskList``, and its ``content`` is a
38
+ list.
39
+ """
40
+ if not isinstance(item, dict):
41
+ return False
42
+ return item.get(key.TYPE) == kind.TASK_LIST and isinstance(
43
+ item.get(key.CONTENT), list
44
+ )
45
+
46
+
47
+ def is_item(node: Any) -> bool:
48
+ """True iff ``node`` is a ``taskItem`` dict.
49
+
50
+ Safe on missing keys: ``node`` must be a dict whose ``type`` is
51
+ ``taskItem``.
52
+ """
53
+ if not isinstance(node, dict):
54
+ return False
55
+ return node.get(key.TYPE) == kind.TASK_ITEM
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tiptap_python_utils
3
- Version: 0.5.0
4
- Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
3
+ Version: 0.6.0
4
+ Summary: Pure-Python utilities for processing TipTap JSON on the server side. Parse, traverse, edit, and serialize TipTap documents — no JavaScript bridge required. Zero runtime dependencies, Python 3.9+.
5
5
  Author: tiptap_python_utils contributors
6
6
  License: MIT License
7
7
 
@@ -56,23 +56,10 @@ Dynamic: license-file
56
56
  [![CI](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/tugkanpilka/tiptap-python-utils/actions/workflows/ci.yml)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
58
58
 
59
- Python utilities for [TipTap](https://tiptap.dev) JSON content.
60
-
61
- `tiptap_python_utils` parses TipTap documents into typed, immutable Python
62
- nodes, preserves unknown/custom nodes for lossless round trips, and provides
63
- small helpers for traversal, immutable edits, visible text extraction, task
64
- queries, and shared-node synchronization.
65
-
66
- - **Zero runtime dependencies.** Standard library only.
67
- - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
68
- - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
69
- - **Immutable AST.** All mutations return new instances via a fluent selection API.
70
-
71
- ## Install
72
-
73
- ```bash
74
- pip install tiptap_python_utils
75
- ```
59
+ TipTap is a JavaScript editor. If your backend is Python and you need to
60
+ process TipTap JSON — extract text, query tasks, sync shared nodes — this
61
+ library does it in pure Python with zero dependencies. No JS bridge, no
62
+ Node.js subprocess.
76
63
 
77
64
  ## Quick Start
78
65
 
@@ -94,8 +81,24 @@ raw = {
94
81
  updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
95
82
  ```
96
83
 
84
+ ## Features
85
+
86
+ - **Zero runtime dependencies.** Standard library only.
87
+ - **Python 3.9+.** Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
88
+ - **Lossless round trip.** Unknown node kinds and any extra fields are preserved.
89
+ - **Immutable AST.** All mutations return new instances via a fluent selection API.
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ pip install tiptap_python_utils
95
+ ```
96
+
97
97
  ## Three Ways to Load a Document
98
98
 
99
+ Pick a constructor by how much you trust the input — lenient, strict, or
100
+ auto-wrapping a bare node into a `doc`.
101
+
99
102
  | Constructor | When to use | On invalid input |
100
103
  |---|---|---|
101
104
  | `Content.parse(raw)` | Lenient — `raw` may be `None`, a string, or a dict | Returns a `Content` with `root=None` |
@@ -104,7 +107,8 @@ updated = Content.require(raw).where_id("p1").leaf().text("New").dump()
104
107
 
105
108
  ## Lossless Round Trip
106
109
 
107
- Parsing never silently drops fields. Two mechanisms preserve information:
110
+ Parsing never silently drops fields custom nodes and unknown keys survive a
111
+ parse-then-serialize cycle byte-for-byte. Two mechanisms preserve information:
108
112
 
109
113
  - `Node.extra` stores top-level keys that aren't part of the known schema
110
114
  (e.g. custom node attributes, vendor-specific keys).
@@ -125,7 +129,8 @@ assert Content.require(raw).to_dict() == raw # byte-for-byte
125
129
 
126
130
  ## Typed Nodes
127
131
 
128
- Build typed nodes directly and serialize them back to TipTap-compatible JSON:
132
+ Build typed nodes directly in Python and serialize them back to
133
+ TipTap-compatible JSON.
129
134
 
130
135
  ```python
131
136
  from tiptap_python_utils import Content, Paragraph, Text
@@ -136,8 +141,8 @@ doc = Content.wrap(node.raw())
136
141
 
137
142
  ## Selection and Editing
138
143
 
139
- The fluent selection API is the single home for mutation. Selection methods
140
- return a new `Content`; the original is never mutated.
144
+ The fluent selection API is the single home for mutation: every method returns
145
+ a new `Content`, so the original is never mutated.
141
146
 
142
147
  ### Select by id or kind
143
148
 
@@ -206,6 +211,9 @@ content.replace_by_id("p1", {
206
211
 
207
212
  ## Text Extraction
208
213
 
214
+ Pull the visible plain text out of a document — useful for search indexing,
215
+ word counts, or previews.
216
+
209
217
  ```python
210
218
  from tiptap_python_utils import Content, text_slices, visible_text, word_count
211
219
 
@@ -218,6 +226,9 @@ slices = text_slices(content, context=True)
218
226
 
219
227
  ## Tasks
220
228
 
229
+ Query task lists in a document — find every task item or check whether any are
230
+ still open.
231
+
221
232
  ```python
222
233
  from tiptap_python_utils import Content, has_open_tasks, open_tasks
223
234
 
@@ -240,6 +251,9 @@ task.shared_id # sharedId attr, if any
240
251
 
241
252
  ## Shared-Node Synchronization
242
253
 
254
+ Keep copies of the same logical node (linked by `sharedId`) in sync — collect
255
+ canonical bodies, then rewrite every matching node from them.
256
+
243
257
  `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
244
258
  a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
245
259
  matching node in the document from those canonical bodies, preserving
@@ -278,17 +292,6 @@ Related helpers on `Content`:
278
292
  - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
279
293
  - `new_shared_id()` — mint a fresh `shared-…` identifier.
280
294
 
281
- ## Architecture (one paragraph)
282
-
283
- The package is layered: `contract` (key/kind/policy primitives) → `model`
284
- (immutable AST with a registry of node classes; unknown kinds round-trip via
285
- `Unknown`) → `codec` (raw I/O in `raw.py`, hydration in `reader.py`, dump in
286
- `writer.py`) → `walk` & `tree` (traversal + path-based replacement on the
287
- immutable tree) → `select` (fluent `Selection` — the single home for mutation)
288
- → `content` (public facade) → `text` / `tasks` / `shared` (user-facing
289
- workflows built on `Content`). All nodes are `@dataclass(frozen=True)`;
290
- mutations return new instances.
291
-
292
295
  ## Public API
293
296
 
294
297
  Common imports are available from the package root:
@@ -311,35 +314,21 @@ from tiptap_python_utils import (
311
314
  )
312
315
  ```
313
316
 
314
- ## Stability
315
-
316
- The project is pre-1.0; minor versions may include breaking changes. See
317
- [CHANGELOG.md](CHANGELOG.md) for what changed and when.
318
-
319
- ## Development
320
-
321
- ```bash
322
- python -m venv .venv
323
- . .venv/bin/activate
324
- python -m pip install -e ".[dev]"
325
- pytest -q
326
- ```
327
-
328
- Build and validate a release artifact:
329
-
330
- ```bash
331
- python -m build
332
- python -m twine check dist/*
333
- ```
334
-
335
317
  ## Contributing
336
318
 
337
319
  Issues and pull requests are welcome. Please read
338
- [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup and release checklist,
339
- and open an issue at
320
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup, architecture overview,
321
+ and release checklist, and open an issue at
340
322
  [github.com/tugkanpilka/tiptap-python-utils/issues](https://github.com/tugkanpilka/tiptap-python-utils/issues)
341
- before larger changes so we can align on the approach.
323
+ before opening a pull request so we can align on the approach.
342
324
 
343
325
  ## License
344
326
 
345
327
  MIT — see [LICENSE](LICENSE).
328
+
329
+ ## Stability
330
+
331
+ The project is pre-1.0; minor versions may include breaking changes. See
332
+ [CHANGELOG.md](CHANGELOG.md) for what changed and when.
333
+ </content>
334
+ </invoke>
@@ -7,6 +7,7 @@ src/tiptap_python_utils/content.py
7
7
  src/tiptap_python_utils/exceptions.py
8
8
  src/tiptap_python_utils/identity.py
9
9
  src/tiptap_python_utils/py.typed
10
+ src/tiptap_python_utils/task.py
10
11
  src/tiptap_python_utils/types.py
11
12
  src/tiptap_python_utils.egg-info/PKG-INFO
12
13
  src/tiptap_python_utils.egg-info/SOURCES.txt
@@ -48,4 +49,5 @@ tests/test_filter.py
48
49
  tests/test_generic_helpers.py
49
50
  tests/test_mutations.py
50
51
  tests/test_public_api.py
52
+ tests/test_task_namespace.py
51
53
  tests/test_traverser.py
@@ -66,6 +66,7 @@ SUBPACKAGE_PUBLIC_NAMES: dict[str, frozenset[str]] = {
66
66
  "new_shared_id",
67
67
  }
68
68
  ),
69
+ "tiptap_python_utils.task": frozenset({"create_list", "is_item", "is_list"}),
69
70
  "tiptap_python_utils.tasks": frozenset(
70
71
  {"has_open_tasks", "open_tasks", "syncable_tasks"}
71
72
  ),
@@ -50,6 +50,7 @@ EXPECTED_PUBLIC_API = frozenset(
50
50
  "open_tasks",
51
51
  "registry",
52
52
  "syncable_tasks",
53
+ "task",
53
54
  "text_slices",
54
55
  "tiptap_id",
55
56
  "visible_text",
@@ -0,0 +1,90 @@
1
+ """Unit tests for the pure raw-dict `task` namespace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tiptap_python_utils import task
6
+
7
+
8
+ # --- create_list -----------------------------------------------------------
9
+
10
+
11
+ def test_create_list_wraps_items_in_task_list_block():
12
+ items = [{"type": "taskItem"}]
13
+ block = task.create_list(items)
14
+ assert block == {"type": "taskList", "content": items}
15
+
16
+
17
+ def test_create_list_accepts_empty_list():
18
+ assert task.create_list([]) == {"type": "taskList", "content": []}
19
+
20
+
21
+ def test_create_list_stores_items_by_reference_not_a_copy():
22
+ items = [{"type": "taskItem", "n": 1}]
23
+ block = task.create_list(items)
24
+ # Same object, not a copy.
25
+ assert block["content"] is items
26
+
27
+
28
+ def test_create_list_reflects_later_mutations_of_original_list():
29
+ items = [{"type": "taskItem", "n": 1}]
30
+ block = task.create_list(items)
31
+ # Caller keeps appending to the original list after wrapping.
32
+ items.append({"type": "taskItem", "n": 2})
33
+ # The change is visible inside the block (by-reference contract).
34
+ assert block["content"] == [
35
+ {"type": "taskItem", "n": 1},
36
+ {"type": "taskItem", "n": 2},
37
+ ]
38
+ assert len(block["content"]) == 2
39
+
40
+
41
+ # --- is_list ---------------------------------------------------------------
42
+
43
+
44
+ def test_is_list_true_for_valid_task_list():
45
+ assert task.is_list({"type": "taskList", "content": []}) is True
46
+ assert task.is_list({"type": "taskList", "content": [{"type": "taskItem"}]}) is True
47
+
48
+
49
+ def test_is_list_false_for_wrong_type():
50
+ assert task.is_list({"type": "bulletList", "content": []}) is False
51
+
52
+
53
+ def test_is_list_false_when_content_not_a_list():
54
+ assert task.is_list({"type": "taskList", "content": {}}) is False
55
+ assert task.is_list({"type": "taskList", "content": "x"}) is False
56
+
57
+
58
+ def test_is_list_false_on_missing_keys_without_raising():
59
+ assert task.is_list({}) is False
60
+ assert task.is_list({"type": "taskList"}) is False # no content key
61
+ assert task.is_list({"content": []}) is False # no type key
62
+
63
+
64
+ def test_is_list_false_for_non_dict():
65
+ assert task.is_list(None) is False
66
+ assert task.is_list([]) is False
67
+ assert task.is_list("taskList") is False
68
+
69
+
70
+ # --- is_item ---------------------------------------------------------------
71
+
72
+
73
+ def test_is_item_true_for_task_item():
74
+ assert task.is_item({"type": "taskItem"}) is True
75
+ assert task.is_item({"type": "taskItem", "attrs": {"checked": True}}) is True
76
+
77
+
78
+ def test_is_item_false_for_wrong_type():
79
+ assert task.is_item({"type": "taskList"}) is False
80
+ assert task.is_item({"type": "listItem"}) is False
81
+
82
+
83
+ def test_is_item_false_on_missing_keys_without_raising():
84
+ assert task.is_item({}) is False
85
+
86
+
87
+ def test_is_item_false_for_non_dict():
88
+ assert task.is_item(None) is False
89
+ assert task.is_item([]) is False
90
+ assert task.is_item("taskItem") is False