{"_id":"md-to-telegram","_rev":"2-5e6a9941d53f518584311d8c12b2d452","name":"md-to-telegram","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"md-to-telegram","version":"0.1.0","keywords":["telegram","markdown","markdownv2","html","remark","mdast","llm","bot"],"author":{"name":"Igor Katsuba","email":"igor@katsuba.dev"},"license":"MIT","_id":"md-to-telegram@0.1.0","maintainers":[{"name":"defenderbass","email":"katsuba.igor@gmail.com"}],"homepage":"https://github.com/IKatsuba/md-to-telegram#readme","bugs":{"url":"https://github.com/IKatsuba/md-to-telegram/issues"},"dist":{"shasum":"202ca3de03ca00164fb4ea81712c9b755c41da08","tarball":"https://registry.npmjs.org/md-to-telegram/-/md-to-telegram-0.1.0.tgz","fileCount":11,"integrity":"sha512-CZ9FrCZZFQlYgb5SubFoFN7IHh9MT/zmvo1dHn/OyeG26jYocaSEp3qGPVvP49Jpydp7RegfmWxMSNT0OtFhEA==","signatures":[{"sig":"MEQCIBusrQjddJ8xVdF0JZVhFBGm5PLSOM+NgOJ14gbw7QxKAiA6YVxZGEV9nxv+4ruM/T8EmBSQ9sgeFUoo7sAC2vMz3Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/md-to-telegram@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":294734},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.mjs","engines":{"node":">=22.18.0"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"97d3acac9cb5940e623556d937189d407be18c63","scripts":{"dev":"tsdown --watch","lint":"eslint .","test":"vitest run","build":"tsdown","format":"prettier --write .","release":"pnpm build && changeset publish","version":"changeset version","test:cov":"vitest run --coverage","changeset":"changeset","typecheck":"tsc --noEmit","test:watch":"vitest","test:mutation":"stryker run","prepublishOnly":"pnpm build"},"_npmUser":{"name":"defenderbass","email":"katsuba.igor@gmail.com"},"repository":{"url":"git+https://github.com/IKatsuba/md-to-telegram.git","type":"git"},"_npmVersion":"11.18.0","description":"Convert LLM-style Markdown into Telegram HTML or MarkdownV2, with a typed report of removed/unsupported constructs.","directories":{},"sideEffects":false,"_nodeVersion":"24.17.0","dependencies":{"unified":"^11.0.5","remark-gfm":"^4.0.1","remark-math":"^6.0.0","remark-parse":"^11.0.0","mdast-util-to-string":"^4.0.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.4","devDependencies":{"eslint":"^10.6.0","tsdown":"^0.22.3","vitest":"^4.1.9","prettier":"^3.9.1","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.0.1","@types/mdast":"^4.0.4","@types/unist":"^3.0.3","@changesets/cli":"^2.31.0","typescript-eslint":"^8.62.0","@vitest/coverage-v8":"^4.1.9","@stryker-mutator/core":"^9.6.1","eslint-config-prettier":"^10.1.8","@stryker-mutator/vitest-runner":"^9.6.1"},"_npmOperationalInternal":{"tmp":"tmp/md-to-telegram_0.1.0_1782921079638_0.11466279998272721","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"md-to-telegram","version":"0.1.1","description":"Convert LLM-style Markdown into Telegram HTML or MarkdownV2, with a typed report of removed/unsupported constructs.","keywords":["telegram","markdown","markdownv2","html","remark","mdast","llm","bot"],"license":"MIT","author":{"name":"Igor Katsuba","email":"igor@katsuba.dev"},"repository":{"type":"git","url":"git+https://github.com/IKatsuba/md-to-telegram.git"},"bugs":{"url":"https://github.com/IKatsuba/md-to-telegram/issues"},"homepage":"https://github.com/IKatsuba/md-to-telegram#readme","type":"module","sideEffects":false,"engines":{"node":">=22.18.0"},"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.cts","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public","provenance":true},"dependencies":{"mdast-util-to-string":"^4.0.0","remark-gfm":"^4.0.1","remark-math":"^6.0.0","remark-parse":"^11.0.0","unified":"^11.0.5"},"devDependencies":{"@changesets/cli":"^2.31.0","@eslint/js":"^10.0.1","@stryker-mutator/core":"^9.6.1","@stryker-mutator/vitest-runner":"^9.6.1","@types/mdast":"^4.0.4","@types/node":"^26.0.1","@types/unist":"^3.0.3","@vitest/coverage-v8":"^4.1.9","eslint":"^10.6.0","eslint-config-prettier":"^10.1.8","prettier":"^3.9.1","tsdown":"^0.22.3","typescript":"^6.0.3","typescript-eslint":"^8.62.0","vitest":"^4.1.9"},"scripts":{"build":"tsdown","dev":"tsdown --watch","typecheck":"tsc --noEmit","lint":"eslint .","format":"prettier --write .","test":"vitest run","test:watch":"vitest","test:cov":"vitest run --coverage","test:mutation":"stryker run","changeset":"changeset","version":"changeset version","release":"pnpm build && changeset publish"},"_id":"md-to-telegram@0.1.1","_integrity":"sha512-WirWplnqN6rMXxkvrqSC4Y6LAs+G/+8ttAAFyBoE55CivL79uiUH6BLgPE9kG6gzrk8fOZk28eebqHkabVHgFA==","_resolved":"/tmp/ff02441985f99dc1b4e2eeb14291384b/md-to-telegram-0.1.1.tgz","_from":"file:md-to-telegram-0.1.1.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-WirWplnqN6rMXxkvrqSC4Y6LAs+G/+8ttAAFyBoE55CivL79uiUH6BLgPE9kG6gzrk8fOZk28eebqHkabVHgFA==","shasum":"58d751216aafc997ed0221993cf6c95ad450f1a9","tarball":"https://registry.npmjs.org/md-to-telegram/-/md-to-telegram-0.1.1.tgz","fileCount":11,"unpackedSize":294661,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/md-to-telegram@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEU93RjG0+P69RYhQVDRMveOGeLdFReooXxpzUO/ODaHAiBfhTuSPLiMGSceHsilqBYnkmb4NORII/dD7M7USmm/AA=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5eec1b52-f383-4f32-be8c-0a0b3e4ec7e3"}},"directories":{},"maintainers":[{"name":"defenderbass","email":"katsuba.igor@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/md-to-telegram_0.1.1_1782922559396_0.6692050874824749"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-01T15:51:19.426Z","modified":"2026-07-01T16:15:59.870Z","0.1.0":"2026-07-01T15:51:19.775Z","0.1.1":"2026-07-01T16:15:59.546Z"},"bugs":{"url":"https://github.com/IKatsuba/md-to-telegram/issues"},"author":{"name":"Igor Katsuba","email":"igor@katsuba.dev"},"license":"MIT","homepage":"https://github.com/IKatsuba/md-to-telegram#readme","keywords":["telegram","markdown","markdownv2","html","remark","mdast","llm","bot"],"repository":{"type":"git","url":"git+https://github.com/IKatsuba/md-to-telegram.git"},"description":"Convert LLM-style Markdown into Telegram HTML or MarkdownV2, with a typed report of removed/unsupported constructs.","maintainers":[{"name":"defenderbass","email":"katsuba.igor@gmail.com"}],"readme":"# md-to-telegram\n\n[![CI](https://github.com/ikatsuba/md-to-telegram/actions/workflows/ci.yml/badge.svg)](https://github.com/ikatsuba/md-to-telegram/actions/workflows/ci.yml)\n\nConvert LLM-style Markdown (GFM + LaTeX math) into **Telegram-renderable** output —\nin both Telegram formats — with a **typed report of everything that had no Telegram\nequivalent**.\n\n- ✅ Targets Telegram **HTML**, **MarkdownV2** (`parse_mode`), and **Rich Markdown**\n  (the `markdown` field of `InputRichMessage`, Bot API 10.1 Rich Messages).\n- ✅ Fully typed API; the result tells you exactly what was dropped (images, math,\n  footnotes, unsupported HTML) and where.\n- ✅ Generates an **LLM prompt** so a model writes convert-friendly Markdown.\n- ✅ Understands Telegram-only directives (spoiler, underline, expandable quote).\n- ✅ Dual ESM + CJS, no peer setup.\n\nThe conversion rules come straight from the specs in [`docs/`](./docs):\n[Telegram formatting](./docs/telegram-formatting-spec.md),\n[LLM Markdown](./docs/llm-markdown-spec.md), and the\n[conversion mapping](./docs/conversion-mapping.md).\n\n## Install\n\n```sh\npnpm add md-to-telegram   # or npm i / yarn add\n```\n\nRequires Node ≥ 22.18.\n\n## Usage\n\n```ts\nimport { toTelegramHTML, toTelegramMarkdownV2 } from \"md-to-telegram\";\n\nconst md = \"# Hello\\n\\nSome **bold** and a ![pic](https://x/y.png) and $a^2$.\";\n\nconst { text, removed } = toTelegramHTML(md);\n// text:    \"<b>Hello</b>\\n\\nSome <b>bold</b> and a  and .\"\n// removed: [{ kind: \"image\", url: \"https://x/y.png\", alt: \"pic\", position: {…} },\n//           { kind: \"math\", value: \"a^2\", inline: true, position: {…} }]\n\ntoTelegramMarkdownV2(md).text;\n// \"*Hello*\\n\\nSome *bold* and a  and \\\\.\"\n```\n\n`convert` is the same thing with an explicit format (handy when the target is dynamic):\n\n```ts\nimport { convert } from \"md-to-telegram\";\nconvert(md, { format: \"markdownv2\" });\n```\n\n### Rich Messages (Bot API 10.1)\n\nFor clients that support [Rich Messages](https://core.telegram.org/bots/api#rich-messages),\nTelegram renders headings, lists, tables, math, images, and footnotes **natively** — so\nthere's almost nothing to drop. `toTelegramRich` produces the string for the `markdown`\nfield of `InputRichMessage`:\n\n```ts\nimport { toTelegramRich, validateRichMarkdown } from \"md-to-telegram\";\n\nconst { text, removed } = toTelegramRich(md);\n// removed is always [] — Rich Markdown is GFM-compatible, so it's near pass-through.\n\n// Length / structural limits are checked separately (it never splits for you):\nconst warnings = validateRichMarkdown(text); // RichLimitWarning[] (empty if within limits)\n\nawait bot.api.sendRichMessage(chatId, { markdown: text });\n```\n\nUse `rich` for capable clients and keep `html` / `markdownv2` as the fallback. When you\ngenerate the source with an LLM, pass `target: \"rich\"` to `buildTelegramPrompt` so the\nmodel is told it may use images, math, tables, and footnotes. An expandable blockquote\n(`> [!expandable]`) becomes a collapsible `<details>` (label via the `expandableSummary`\noption).\n\n### Long messages (splitting)\n\nTelegram rejects messages over a length limit (4096 for `parse_mode`, 32768 for Rich).\n`splitMessage` breaks rendered output into parts that fit **without corrupting markup** —\nit packs on block boundaries, re-wraps oversized code blocks, and closes/reopens any open\ntags or inline marks at a seam:\n\n```ts\nimport { splitMessage, toTelegramHTML } from \"md-to-telegram\";\n\nconst { text } = toTelegramHTML(longMarkdown);\nfor (const part of splitMessage(text, { format: \"html\" })) {\n  await bot.api.sendMessage(chatId, part, { parse_mode: \"HTML\" });\n}\n```\n\nPass `maxLength` to override the per-format default.\n\n### Streaming\n\nBot API 9.3+ can stream a reply with `sendMessageDraft` (and `sendRichMessageDraft` for\nRich). `sendMessageDraft` takes `parse_mode`, so stream a **formatted** preview: convert\nthe partial buffer each tick (`toTelegramHTML(buffer)`) and send it with `parse_mode`.\nThis is safe because `convert` always emits valid markup even from half-written Markdown\n(an unclosed `**bold` stays literal until it closes — no 400s mid-stream). Reuse one\nnon-zero `draft_id` so updates animate, then send the final converted message once. See\n[`examples/ai-sdk-grammy.ts`](./examples/ai-sdk-grammy.ts) for the full draft → finalize\nflow with the Vercel AI SDK + grammY.\n\n### Handling what was removed\n\n`removed` is a discriminated union — narrow on `kind` to get exact, typed fields:\n\n```ts\nconst { text, removed } = toTelegramHTML(md);\n\nfor (const item of removed) {\n  switch (item.kind) {\n    case \"image\":\n      await bot.sendPhoto(chatId, item.url, { caption: item.alt });\n      break;\n    case \"math\":\n      console.warn(`dropped ${item.inline ? \"inline\" : \"block\"} math: ${item.value}`);\n      break;\n    case \"footnote\": // item.identifier, item.variant, item.value\n    case \"html\": // item.tagName, item.scope, item.value\n      break;\n  }\n}\n```\n\n### Recommended pipeline: LLM → Markdown → convert\n\nThe most reliable setup is to let the model write **plain Markdown** and convert it\ndeterministically — no post-processing or \"sanitizing\" of model output needed.\n`buildTelegramPrompt()` produces a single, format-agnostic system prompt that keeps the\nmodel away from unconvertible constructs and teaches it the Telegram-only directives:\n\n```ts\nimport { buildTelegramPrompt, toTelegramHTML } from \"md-to-telegram\";\n\nconst system = buildTelegramPrompt(); // \"write Markdown, avoid images/math/...; you may use ||spoiler||, ...\"\nconst markdown = await llm({ system, prompt: userTask });\n\nconst { text } = toTelegramHTML(markdown); // always valid Telegram HTML\nawait bot.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n```\n\n### Telegram-only directives\n\nStandard Markdown can't express some Telegram entities, so this library understands a\nfew small extensions (also documented in the generated prompt). They work in **both**\noutput formats:\n\n| Directive | Markdown syntax | HTML | MarkdownV2 |\n|---|---|---|---|\n| Spoiler | `\\|\\|text\\|\\|` | `<tg-spoiler>` | `\\|\\|text\\|\\|` |\n| Underline | `++text++` | `<u>` | `__text__` |\n| Expandable quote | a blockquote starting with a `> [!expandable]` line | `<blockquote expandable>` | `**>…\\|\\|` |\n\n## What maps to what\n\n`md-to-telegram` re-serializes from a parsed AST, so Telegram entities are always\nwell-formed and correctly escaped. Highlights:\n\n- **Direct:** bold, italic, bold+italic, strikethrough, inline code, links, code blocks,\n  blockquotes.\n- **Approximated:** headings → bold; lists → `•`/numbered/`☑`·`☐`; tables → fixed-width\n  block; thematic break → a rule line; nested blockquotes → flattened (Telegram can't\n  nest them).\n- **Removed + reported** (no Telegram equivalent): images, LaTeX math, footnotes,\n  unsupported raw HTML. Opt into light degradation with options\n  (`images: \"link\"`, `math: \"raw\"`, `footnotes: \"inline\" | \"append\"`).\n\n> Note: `__x__` is **bold** in Markdown but **underline** in Telegram MarkdownV2 —\n> this library always emits bold as `*x*`, so it never silently becomes underline.\n\n## Options\n\nAll options are optional; defaults match `docs/conversion-mapping.md`.\n\n| Option | Default | Description |\n|---|---|---|\n| `tables` | `\"pre\"` | `\"pre\"` (fixed-width block) or `\"remove\"`. |\n| `thematicBreak` | a line of `─` | A literal string, or `\"blank\"` for an empty line. |\n| `flattenBlockquotes` | `true` | Flatten nested blockquotes to one level. |\n| `images` | `\"remove\"` | `\"remove\"` or `\"link\"` (always reported either way). |\n| `math` | `\"remove\"` | `\"remove\"` or `\"raw\"` (keep the LaTeX as code). |\n| `footnotes` | `\"remove\"` | `\"remove\"`, `\"inline\"`, or `\"append\"`. |\n| `collectRemoved` | `true` | Populate `result.removed`. |\n| `listIndent` | `3` | Spaces per nested-list level. |\n| `expandableSummary` | `\"Details\"` | `rich` only: `<summary>` label for `> [!expandable]` quotes. |\n\n## Releasing\n\nCI (lint, typecheck, build, tests, mutation) runs on every PR. Releases use\n[Changesets](https://github.com/changesets/changesets) + npm **trusted publishing (OIDC)**:\n\n1. Add a changeset in your PR: `pnpm changeset` (pick the bump, write a summary).\n2. Merging to `main` opens a **\"Version Packages\"** PR (bumps version + `CHANGELOG.md`).\n3. Merging that PR publishes to npm automatically with provenance — no stored token.\n\nFirst publish only (npm can't configure OIDC for a name that doesn't exist yet): add a\ntemporary `NPM_TOKEN` secret, run the **Bootstrap publish** workflow once, configure the\ntrusted publisher in the npm package settings, then delete the secret.\n\n## License\n\nMIT © Igor Katsuba\n","readmeFilename":"README.md"}