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

Content.shared_families() collects canonical bodies grouped by sharedId into a SharedFamilies value object. Content.sync_shared(families) rewrites every matching node in the document from those canonical bodies, preserving per-instance identity (id, sharedId). Both return immutable values — the original Content is never mutated.

from tiptap_python_utils import Content

# Canonical doc: the source of truth for every shared body.
canonical = Content.require({"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 = Content.require({"type": "doc", "content": [
    {
        "type": "paragraph",
        "attrs": {"id": "p1-copy", "sharedId": "intro"},
        "content": [{"type": "text", "text": "Stale copy"}],
    }
]})

synced = target.sync_shared(canonical.shared_families())
assert synced.has_shared("intro")

Related helpers on Content:

  • content.where_shared_id(sid)Selection over every node with that sharedId.
  • content.has_shared(sid) — quick presence check.
  • node.with_shared_id(sid) — stamp a sharedId onto a node (returns a new node).
  • new_shared_id() — mint a fresh shared-… identifier.

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,
    SharedFamilies,
    TaskItem,
    Text,
    has_open_tasks,
    kind,
    new_shared_id,
    open_tasks,
    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.4.0.tar.gz (29.7 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.4.0-py3-none-any.whl (27.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tiptap_python_utils-0.4.0.tar.gz
  • Upload date:
  • Size: 29.7 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.4.0.tar.gz
Algorithm Hash digest
SHA256 e0e9d6fb89236abf7a0e128f4ceabfee0860924b58c11e722d91a21a037ab7af
MD5 9f4b81a655665dd162a40b179b649720
BLAKE2b-256 5b059f321ca797aa018d2de087bfa00831283d374450e3606c6bcebf9831495f

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiptap_python_utils-0.4.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.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for tiptap_python_utils-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 888ef45fc4266de493a8839290a3c629faa75360bca007793421e8d21865455b
MD5 a57058691674fe03b52b9a2ab6b95f0a
BLAKE2b-256 7a315d4e82332b274a2ea9f457ba50a96b052c1d7832052edae978dd44e8bd58

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiptap_python_utils-0.4.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