Advanced formatting add-ons for Telethon with predictable Markdown and HTML parsing
Project description
🧩 Delimiters
Advanced • Lossless Markdown & HTML ↔ Telegram Entities for Telethon
Production-ready, round‑trip safe, two‑phase mention model — built for editors, userbots, mirrors and exporters.
✨ Table of contents
- What is delimiters?
- Why it matters
- Highlights & Features
- Badges & Compatibility
- Install
- Quickstart (three lines)
- Two‑phase mention model (v0.1.1+)
- Premium API Reference (clear & linkable)
- Recipes & Examples
- Best practices & troubleshooting
- Project layout
- Contributing & release checklist
- Author & Maintainers
- License & Links
What is delimiters? 🚀
delimiters is a focused formatting engine that converts between human‑friendly Markdown/HTML and Telegram's native MessageEntity model — reliably and without losing data. Use it whenever you need exact fidelity when sending, editing, archiving, or exporting Telegram content.
Think: "Markdown/HTML as serialization; Telegram entities as the source of truth."
Why it matters ❗
Telegram stores formatting as MessageEntity objects (e.g. MessageEntityBold, MessageEntitySpoiler) — not as Markdown or HTML text. Many tools:
- Break nested entities when editing
- Lose formatting on edits or round-trips
- Mistreat custom emojis and Unicode surrogate pairs
- Perform hidden network calls during parsing
delimiters solves these problems by:
- making parse() pure & offline,
- making mention resolution explicit (network/async),
- preserving nested entities, custom emojis, and collapsed/expanded blockquote state.
Highlights & Features ✨
- Small, focused API: parse / unparse / resolve_mentions
- Full Telegram entity coverage used in chats:
- Bold, Italic, Underline, Strike
- Inline & block code
- Inline spoilers and
<tg-spoiler> - Collapsed & expanded blockquotes (state preserved)
- Mentions (tg://user?id=...), text URLs, custom emojis (
<tg-emoji>)
- Lossless round-trip: Markdown/HTML ↔ (text + entities) ↔ Markdown/HTML
- Deterministic parsing — no network I/O during parse()
- Explicit async mention resolution matching Telethon semantics (v0.1.1+)
- Unicode- and surrogate-pair-safe
- Telethon-friendly (compatible with Telethon ≥ 1.34)
Badges & Compatibility 🧾
- PyPI: https://pypi.org/project/delimiters/
- GitHub repo: https://github.com/ankit-chaubey/delimiters
- Python 3.8+
- Telethon >= 1.34 recommended
Install 📦
From PyPI (recommended):
pip install delimiters
Or install from source:
git clone https://github.com/ankit-chaubey/delimiters.git
cd delimiters
pip install .
Quickstart — three lines ✨
from delimiters import parse, resolve_mentions
text, entities = parse("**Hi** [User](tg://user?id=93602376)")
entities = await resolve_mentions(client, entities)
await client.send_message(chat_id, text, formatting_entities=entities)
Two‑phase mention model (v0.1.1+) 🔁
delimiters intentionally mirrors Telethon: parsing and mention resolution are separate.
Phase 1 — Parsing (offline, pure)
text, entities = parse(input_text, mode="md") # or mode="html"
- Deterministic
- No network calls
- Mentions become:
MessageEntityTextUrl("tg://user?id=...")
Phase 2 — Mention resolution (async, network)
entities = await resolve_mentions(client, entities)
- Converts
MessageEntityTextUrl("tg://user?id=...")→InputMessageEntityMentionName(resolved form) - Required before sending or editing messages that should notify users or render clickable mentions
- Matches Telethon's pipeline; intentional design
Important: skip resolve_mentions and the mention stays a text URL — no notifications, no clickable @. This prevents hidden network calls during parse-time.
Premium API Reference (clear, linkable, examples) 🔎
All functions are exported from the top-level package: from delimiters import parse, unparse, resolve_mentions.
- parse(text: str, mode: Literal["md","html"]="md") -> Tuple[str, List[MessageEntity]]
- Purpose: Convert Markdown/HTML → (text, entities).
- Key behavior:
- Pure function; no network I/O.
- Mentions are left unresolved as text-URL entities (
tg://user?id=...). - Supports extended markdown tokens:
!!underline!!,||spoiler||,%%collapsed%%,^^expanded^^.
- Example:
text, entities = parse("**Hello** ||secret|| [AnKiT](tg://user?id=93602376)")
- unparse(text: str, entities: List[MessageEntity], mode: Literal["md","html"]="md") -> str
- Purpose: Convert (text + Telegram entities) → Markdown or HTML.
- Key behavior:
- Preserves nesting and entity boundaries where possible.
- Useful for round-trip editing workflows.
- Example:
md = unparse(message.text, message.entities, mode="md")
- resolve_mentions(client: TelegramClient, entities: List[MessageEntity]) -> List[Union[MessageEntity, InputMessageEntity]]
- Purpose: Convert text-URL mention entities into resolved mention entities accepted by the Telegram API.
- Key behavior:
- Async; requires an active
client(Telethon). - Uses the client to map
tg://user?id=...intoInputMessageEntityMentionNameor equivalent. - Returns entities ready to pass to Telethon
send_messageormessage.editformatting_entitiesparameter.
- Async; requires an active
- Example:
entities = await resolve_mentions(client, entities)
await client.send_message(chat_id, text, formatting_entities=entities)
Notes:
- The returned entity objects are compatible with Telethon’s API.
- If your environment does not require notify/clickable mentions (e.g., drafts or previews), you can skip the resolve step.
Recipes & Examples 🧩
Full send flow (Markdown):
from delimiters import parse, resolve_mentions
md = "**Welcome** ||this is private|| [Bob](tg://user?id=93602376)"
text, entities = parse(md)
entities = await resolve_mentions(client, entities)
await client.send_message(chat_id, text, formatting_entities=entities)
Editing with round-trip safety:
from delimiters import unparse, parse, resolve_mentions
# Get stable markdown representation of existing message
md = unparse(message.text, message.entities, mode="md")
# Make textual edits on md
md = md.replace("old", "new")
# Re-parse and resolve
text, entities = parse(md)
entities = await resolve_mentions(client, entities)
await message.edit(text, formatting_entities=entities)
HTML mode:
html = '<b>Heads up</b> <tg-spoiler>secret</tg-spoiler> <a href="tg://user?id=93602376">Eve</a>'
text, entities = parse(html, mode="html")
entities = await resolve_mentions(client, entities)
await client.send_message(chat_id, text, formatting_entities=entities)
Helper utility (combine parse+resolve):
async def parse_and_resolve(client, raw: str, mode: str = "md"):
text, entities = parse(raw, mode=mode)
return text, await resolve_mentions(client, entities)
Round-trip test (use in CI):
md = unparse(orig_text, orig_entities, mode="md")
text2, entities2 = parse(md)
# After resolve_mentions(client, entities2), compare semantics with original entities
Best practices & troubleshooting 🛠️
- Always call parse() as a pure function (no network).
- Always call resolve_mentions(...) immediately before sending/editing to ensure mentions are clickable and notify.
- Use unparse() → edit → parse() to preserve nested entities and collapsed/expanded blockquote state.
- If mentions are not notifying: make sure you resolved mentions and the client has necessary access.
- If formatting breaks on edit: ensure you pass
formatting_entities=entitieswhen editing and that entities were resolved appropriately. - Test with surrogate pairs + custom emojis during CI to avoid rendering surprises.
Project layout 🗂️
delimiters/
├── __init__.py # Public API
├── api.py # High-level parse/unparse/resolve_mentions
├── custom_markdown.py # Markdown ↔ entities rules
├── markdown_ext.py # Extra delimiters (underline, spoilers, blockquotes)
├── html_ext.py # HTML ↔ entities rules
├── tests/ # Unit tests (recommended)
├── setup.py
├── LICENSE
└── README.md
Contributing & release checklist 🤝
Contributions welcome — please open an issue first to discuss bigger changes.
PR checklist:
- Add/extend tests for Unicode, nested entities, custom emoji, collapsed quotes.
- Keep parse() offline and deterministic.
- Keep resolve_mentions() explicitly network-bound.
- Update CHANGELOG.md and bump version (semver).
- Run packaging checks:
- python -m build
- python -m pip install dist/delimiters--py3-none-any.whl
- Publish:
- twine upload dist/*
Changelog (summary) 📝
- v0.1.1 — Introduced explicit two‑phase mention resolution:
- parse() is offline-only and no longer resolves mentions.
- resolve_mentions(client, entities) added to explicitly resolve tg://user?id references to proper mention entities.
(Check CHANGELOG.md in the repo for detailed history.)
Author / Creator / Maintainer 👨💻
✨ Creator & Maintainer
Ankit Chaubey
Profile
- Email: m.ankitchaubey@gmail.com
- Telegram: @ankify
- Personal: GitHub Page
License & Links 📜
- License: MIT License © 2026 Ankit Chaubey
- PyPI: https://pypi.org/project/delimiters/
- Repository: https://github.com/ankit-chaubey/delimiters
- Issues: https://github.com/ankit-chaubey/delimiters/issues
Thank you for using delimiters! built for clarity, correctness, and production workflows ❤️
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file delimiters-0.1.3.tar.gz.
File metadata
- Download URL: delimiters-0.1.3.tar.gz
- Upload date:
- Size: 16.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4f2b20dd42152462c51d18bfb2ad46b3c0868ef5175ec9cd3c23ee13d871e53
|
|
| MD5 |
f51b4a13da7a8163f9effc353717df75
|
|
| BLAKE2b-256 |
80599085562fde7b5cfeef68157bfe2fe2376405816ee1a68b5e895db8fb8444
|
Provenance
The following attestation bundles were made for delimiters-0.1.3.tar.gz:
Publisher:
publish.yml on ankit-chaubey/delimiters
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
delimiters-0.1.3.tar.gz -
Subject digest:
a4f2b20dd42152462c51d18bfb2ad46b3c0868ef5175ec9cd3c23ee13d871e53 - Sigstore transparency entry: 813295174
- Sigstore integration time:
-
Permalink:
ankit-chaubey/delimiters@d438208e73aab4f074e364c7d1b7601b233a7ee4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ankit-chaubey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d438208e73aab4f074e364c7d1b7601b233a7ee4 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file delimiters-0.1.3-py3-none-any.whl.
File metadata
- Download URL: delimiters-0.1.3-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46d69561284b5257628b112fc4eb36de5b9408f292a4bd2e172ff263d36624c4
|
|
| MD5 |
89062edbc0e7c25cecba911cb6964886
|
|
| BLAKE2b-256 |
a60b6ef79c71f12fa3e4f789909e35a2b64b5ddd5fbfeab4592ec829d0b5c05d
|
Provenance
The following attestation bundles were made for delimiters-0.1.3-py3-none-any.whl:
Publisher:
publish.yml on ankit-chaubey/delimiters
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
delimiters-0.1.3-py3-none-any.whl -
Subject digest:
46d69561284b5257628b112fc4eb36de5b9408f292a4bd2e172ff263d36624c4 - Sigstore transparency entry: 813295177
- Sigstore integration time:
-
Permalink:
ankit-chaubey/delimiters@d438208e73aab4f074e364c7d1b7601b233a7ee4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ankit-chaubey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d438208e73aab4f074e364c7d1b7601b233a7ee4 -
Trigger Event:
workflow_dispatch
-
Statement type: