Skip to main content

Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.

Project description

tiptap_python_utils

PyPI Python CI License: MIT

Python utilities for TipTap JSON content.

tiptap_python_utils parses TipTap documents into typed, immutable Python nodes, preserves unknown/custom nodes for lossless round trips, and provides small helpers for traversal, immutable edits, visible text extraction, task queries, and shared-node synchronization.

  • Zero runtime dependencies. Standard library only.
  • Python 3.9+. Tested on 3.9, 3.10, 3.11, 3.12, 3.13.
  • Lossless round trip. Unknown node kinds and any extra fields are preserved.
  • Immutable AST. All mutations return new instances via a fluent selection API.

Install

pip install tiptap_python_utils

Quick Start

from tiptap_python_utils import Content

raw = {
    "type": "doc",
    "content": [
        {
            "type": "paragraph",
            "attrs": {"id": "p1"},
            "content": [{"type": "text", "text": "Old"}],
        }
    ],
}

# Strict-load → descend to the text leaf → write a new value → serialize.
updated = Content.require(raw).where_id("p1").leaf().text("New").dump()

Three Ways to Load a Document

Constructor When to use On invalid input
Content.parse(raw) Lenient — raw may be None, a string, or a dict Returns a Content with root=None
Content.require(raw) Strict — input must be a valid TipTap doc Raises TiptapValidationError
Content.wrap(node) Auto-wraps a non-doc node into a doc root Raises if the node is not parseable

Lossless Round Trip

Parsing never silently drops fields. Two mechanisms preserve information:

  • Node.extra stores top-level keys that aren't part of the known schema (e.g. custom node attributes, vendor-specific keys).
  • Node.present records which structural keys (attrs, content, …) appeared in the raw input, so raw() emits empty attrs: {} or content: [] only when they were originally present.
  • Unknown node kinds become Unknown(raw_kind="…") rather than being rejected.
from tiptap_python_utils import Content

raw = {"type": "doc", "content": [
    {"type": "customPanel", "attrs": {"id": "p1"}, "content": [], "custom": {"x": 1}}
]}

assert Content.require(raw).to_dict() == raw  # byte-for-byte

Typed Nodes

Build typed nodes directly and serialize them back to TipTap-compatible JSON:

from tiptap_python_utils import Content, Paragraph, Text

node = Paragraph(id="p1", content=(Text(value="Hello"),))
doc = Content.wrap(node.raw())

Selection and Editing

The fluent selection API is the single home for mutation. Selection methods return a new Content; the original is never mutated.

Select by id or kind

from tiptap_python_utils import Content, kind

# By id (uses TipTap's id resolution rules under the hood).
content.where_id("p1")

# By TipTap kind.
content.of(kind.PARAGRAPH)

Atomic mutations

# Write an attribute on the selected node.
content.where_id("p1").attr("color", "blue")

# Descend to the first text descendant, then write text or marks.
content.where_id("p1").leaf().text("Updated")
content.where_id("p1").leaf().marks([{"type": "bold"}])

# Replace the whole selected node, or append a child to it.
content.where_id("p1").replace({"type": "paragraph", "attrs": {"id": "p1"}, "content": []})
content.where_id("ul1").append({"type": "listItem", "attrs": {"id": "li-new"}, "content": []})

.text() and .marks() are strict — they only operate on Text refs. Chain .leaf() first to descend from a container.

Document-level commands

# Append a node to the document root.
content.append_root({"type": "paragraph", "attrs": {"id": "p2"}, "content": []})

# Replace a node by id (the replacement's attrs.id must match).
content.replace_by_id("p1", {
    "type": "paragraph",
    "attrs": {"id": "p1"},
    "content": [{"type": "text", "text": "Replaced"}],
})

Text Extraction

from tiptap_python_utils import Content, text_slices, visible_text, word_count

content = Content.require(raw)

plain_text = visible_text(content)
count = word_count(content)
slices = text_slices(content, context=True)

Tasks

from tiptap_python_utils import Content, has_open_tasks, open_tasks

content = Content.require(raw)

pending = has_open_tasks(content)
items = open_tasks(content)

Each TaskItem exposes derived state as properties:

task = open_tasks(content)[0]

task.task_item_id       # canonical id (falls back to local id)
task.is_completed       # status / checked interpretation
task.is_linked_copy     # True when local id differs from canonical id
task.shared_id          # sharedId attr, if any

Shared-Node Synchronization

shared_families collects canonical bodies grouped by sharedId; sync_shared rewrites every matching node in a document using those canonical bodies while preserving per-instance identity (id, sharedId).

from tiptap_python_utils import shared_families, sync_shared

# Canonical doc: the source of truth for every shared body.
canonical = {"type": "doc", "content": [
    {
        "type": "paragraph",
        "attrs": {"id": "p1", "sharedId": "intro"},
        "content": [{"type": "text", "text": "Authoritative intro"}],
    }
]}

# Doc that mirrors the same sharedId but with a stale body.
target = {"type": "doc", "content": [
    {
        "type": "paragraph",
        "attrs": {"id": "p1-copy", "sharedId": "intro"},
        "content": [{"type": "text", "text": "Stale copy"}],
    }
]}

families = shared_families(canonical)
synced_json, changed = sync_shared(target, families)
assert changed is True

Architecture (one paragraph)

The package is layered: contract (key/kind/policy primitives) → model (immutable AST with a registry of node classes; unknown kinds round-trip via Unknown) → codec (raw I/O in raw.py, hydration in reader.py, dump in writer.py) → walk & tree (traversal + path-based replacement on the immutable tree) → select (fluent Selection — the single home for mutation) → content (public facade) → text / tasks / shared (user-facing workflows built on Content). All nodes are @dataclass(frozen=True); mutations return new instances.

Public API

Common imports are available from the package root:

from tiptap_python_utils import (
    Content,
    Paragraph,
    TaskItem,
    Text,
    has_open_tasks,
    kind,
    open_tasks,
    shared_families,
    sync_shared,
    text_slices,
    visible_text,
    word_count,
)

Stability

The project is pre-1.0; minor versions may include breaking changes. See CHANGELOG.md for what changed and when.

Development

python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"
pytest -q

Build and validate a release artifact:

python -m build
python -m twine check dist/*

Contributing

Issues and pull requests are welcome. Please read CONTRIBUTING.md for the local setup and release checklist, and open an issue at github.com/tugkanpilka/tiptap-python-utils/issues before larger changes so we can align on the approach.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tiptap_python_utils-0.2.0.tar.gz (29.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tiptap_python_utils-0.2.0-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file tiptap_python_utils-0.2.0.tar.gz.

File metadata

  • Download URL: tiptap_python_utils-0.2.0.tar.gz
  • Upload date:
  • Size: 29.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tiptap_python_utils-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4caf7e2c42419a35ab5ac4e888d4851d0f46ebc978d924e1b1bbb1898f86b9b3
MD5 56b739ac80a951cf947e87f4dd92bcc0
BLAKE2b-256 5b80054784810bbc90a416f88684751e86a20bde78f39d9be8a902dbd59016c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiptap_python_utils-0.2.0.tar.gz:

Publisher: publish.yml on tugkanpilka/tiptap-python-utils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tiptap_python_utils-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for tiptap_python_utils-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 58171948fe281a3867fc4e3e58f5604516cb9c5cfbd0881f51e1bd3174e87b74
MD5 4537d9e2df968c75dc2f21c725a46223
BLAKE2b-256 f110729974f15d46a1ac54a7acbfc8c35696ceed303bd9342c74bcffac6aad20

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiptap_python_utils-0.2.0-py3-none-any.whl:

Publisher: publish.yml on tugkanpilka/tiptap-python-utils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page