Repository navigation
Conversation
Since 4.41, `{ markdown }` and `{ ast }` posts go through
`sendRichMessage` with the markdown passed unchanged. Telegram's
rich-message parser reads `$...$` as inline math, so a message such as
`MU ~$950 (-3.3% from $983.23)` rendered as a formula, and `%` inside
it started a TeX comment that dropped the rest. The parser also treats
a single newline as a soft break, so line-per-item text collapsed into
one paragraph.
`escapeRichMarkdown()` parses the markdown and rewrites only the source
of text nodes: it escapes `$` and ends each soft-wrapped line with a
backslash hard break. Inline code, code blocks, link URLs, and
autolinks keep their source. Posts, edits, and streamed drafts and
final messages all use it, and length checks count the escaped text.
Markdown that relied on `$...$` for a formula now shows the dollar
signs, so the docs no longer list formulas as a rich-message feature.
Fixes vercel#1109
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Gourav Kumar Dhiman <gkrdhiman@gmail.com>
|
@Gourav224 is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
| if ("children" in node) { | ||
| for (const child of node.children) { | ||
| collectTextRanges(child, markdown, ranges); |
There was a problem hiding this comment.
Thanks for picking this up. The core approach (rewrite only the source of mdast text nodes) is sound, and most of the edge cases I threw at it behave well. Two things need to change before merge, plus a few smaller suggestions inline.
What I checked
- Checked out
aa405b44, ran the full@chat-adapter/telegramsuite (524 passed),tsc --noEmitandbiome checkon the changed files: all clean. - Probed
escapeRichMarkdownwith ~35 inputs: existing escapes,\$, code spans and fences,```math, tables, footnotes, task lists, setext headings, lazy blockquote continuation, inline HTML, entities, bare URLs, emails,www.links, emoji placeholders, and AST input viastringifyMarkdown. Output is idempotent (escape(escape(x)) === escape(x)) for every case, and code, autolinks, and existing hard breaks are preserved. - Mutation testing against
rich.test.ts+index.test.ts:
| Mutant | Result |
|---|---|
Drop $ escaping |
killed |
| Drop newline to hard-break branch | killed |
Drop \\[\s\S] (existing escape) alternative |
killed |
| Drop the autolink skip | killed |
Remove escapeRichMarkdown from renderRichMarkdown |
killed |
Remove escaping from the streamed draft (sendRichMessageDraft) |
survived |
| Remove escaping from the streamed final message | survived |
\r?\n changed to \n |
survived |
- I could not run a live smoke test against the Bot API (no bot token available), so Telegram-side rendering is verified only against the Bot API rich markdown docs, not end to end.
Requested changes
-
Streaming paths are untested. The PR description says streamed drafts and final messages are escaped, but both
escapeRichMarkdowncalls instream()can be deleted with the whole suite still green. Please add a stream test that asserts thesendRichMessageDraftand finalsendRichMessagebodies contain\$and a backslash before each newline. See the inline comments. -
Shortcut and collapsed reference links break (confirming the existing review comment on
rich.ts). The label of[label]/[label][]is both the link text and the lookup key, and CommonMark matches labels on raw source, so escaping it detaches the link from its definition. Reproduced:
"[$5]\n\n[$5]: https://x.com" linkReference -> text (output "[\\$5]")
"[$5][]\n\n[$5]: https://x.com" linkReference -> text
"[a\nb]\n\n[a b]: https://x.com" linkReference -> text, break, text
Full references ([$5][ref]) are fine. Simplest fix: in collectTextRanges, don't descend into linkReference / imageReference nodes whose referenceType is not "full". Alternatively rewrite them to full references ([\$5][$5]) so the text is escaped and the label still resolves. Please add regression tests for both shortcut and collapsed forms.
Non-blocking
<details>HTML blocks are skipped. The Bot API docs say Markdown is parsed inside<details>,<tg-collage>and<tg-slideshow>, but remark treats<details><summary>Price</summary>\nIt costs $5 and $6\n</details>as onehtmlnode, so neither the$nor the line breaks inside it are touched. Content separated from the tags by blank lines is handled correctly. Worth either handling or calling out in the docs, since the docs list details as a rich-message feature.- Split-mode cost. With
longMessages: "split", everyfits()probe insplitLongMessagenow re-parses the stringified part. On a 118 KB, 4,000-line list the post went from ~4.2 s to ~9.4 s locally (default truncate mode on 28 KB: ~400 ms to ~560 ms). Not a blocker, but a cheap fast path or computing the escaped length without a full parse would help.
| let draftText: string; | ||
| if (streamUsesRich) { | ||
| draftText = truncateRichMarkdown(renderer.render()); | ||
| draftText = truncateRichMarkdown(escapeRichMarkdown(renderer.render())); |
There was a problem hiding this comment.
Removing escapeRichMarkdown here keeps all 524 tests passing. Please cover the draft path, e.g. stream "MU ~$950\nTSLA ~$403" and assert the sendRichMessageDraft body's markdown is "MU ~\\$950\\\nTSLA ~\\$403".
|
|
||
| if (streamUsesRich) { | ||
| const markdown = truncateRichMarkdown(finalMarkdown); | ||
| const markdown = truncateRichMarkdown(escapeRichMarkdown(finalMarkdown)); |
There was a problem hiding this comment.
Same here: removing the escape on the final streamed sendRichMessage is not caught by any test. The same stream test can assert the final body too.
| const MARKDOWN_PUNCTUATION = /[!-/:-@[-`{-~]/g; | ||
| const LINE_BREAKS = /[\r\n]/g; | ||
| const BACKTICKS = /`+/g; | ||
| const RICH_TEXT_SPECIALS = /\\[\s\S]|\$|\r?\n/g; |
There was a problem hiding this comment.
Nit: CRLF handling is untested (changing \r?\n to \n survives the suite), and a bare \r line ending, which CommonMark also treats as a line ending, isn't converted ("a\rb" comes back unchanged). Worth a "a\r\nb" test, and either handling \r or noting it's intentionally unsupported.
| ## Markdown formatting | ||
|
|
||
| On Telegram Bot API 10.1 and newer, explicit `{ markdown }` and `{ ast }` messages use rich messages. Standard markdown gains native headings, lists, tables, task lists, formulas, details, and separate media blocks where supported by Telegram. | ||
| On Telegram Bot API 10.1 and newer, explicit `{ markdown }` and `{ ast }` messages use rich messages. Standard markdown gains native headings, lists, tables, task lists, details, and separate media blocks where supported by Telegram. The adapter escapes `$` outside code so Telegram doesn't read it as a formula delimiter, and turns single line breaks into hard breaks so each line stays on its own line, as it does in the MarkdownV2 path. |
There was a problem hiding this comment.
Suggestion: formulas aren't actually gone. Fenced ```math blocks are code nodes, so they pass through unescaped, and the Bot API renders them as formulas (as does inline <tg-math>). Consider keeping formulas in the list and saying they are written with a ```math block rather than $...$. Same for the README.
| ## Markdown formatting | ||
|
|
||
| On Telegram Bot API 10.1 and newer, explicit `{ markdown }` and `{ ast }` messages use rich messages, including native headings, lists, tables, task lists, formulas, details, and separate media blocks supported by the Bot API. | ||
| On Telegram Bot API 10.1 and newer, explicit `{ markdown }` and `{ ast }` messages use rich messages, including native headings, lists, tables, task lists, details, and separate media blocks supported by the Bot API. The adapter escapes `$` outside code so Telegram doesn't read it as a formula delimiter, and turns single line breaks into hard breaks so each line stays on its own line, as it does in the MarkdownV2 path. |
There was a problem hiding this comment.
Same note as the docs page: formulas still work via ```math fenced blocks.
Since 4.41,
{ markdown }and{ ast }posts go throughsendRichMessagewith the markdown passed unchanged. Telegram's rich-message parser differs from CommonMark in two ways that break ordinary text:$...$is inline math, soMU ~$950 (-3.3% from $983.23)renders as a formula, and%inside it starts a TeX comment that drops the rest.• a\n• bcollapses into one paragraph. The MarkdownV2 path kept those lines.This adds
escapeRichMarkdown()to the Telegram adapter. It parses the markdown and rewrites only the source of text nodes:$becomes\$, and each soft line ending gets a trailing backslash so Telegram renders a hard break. The issue reports that both forms render correctly insendRichMessage. Inline code, code blocks, link URLs, and autolinks keep their source. Posts, edits, and streamed drafts and final messages all use it, and the rich-message length checks count the escaped text.Markdown that used
$...$on purpose to render a formula now shows the dollar signs, so the Telegram docs and README no longer list formulas as a rich-message feature.Tests
New unit tests cover dollar amounts,
$inside inline and fenced code, existing escapes and hard breaks, and line breaks in paragraphs, block quotes, and list items, plus an adapter test that checks the markdown sent tosendRichMessage. That adapter test fails without the change.@chat-adapter/telegramvitest: 524 passedbiome checkon the changed files andtsc --noEmitfor the adapter: cleanFixes #1109
🤖 Generated with Claude Code