fix(telegram): move fenced-code-block splitting into Telegram-specific helper
Move the fenced-code-block-aware splitting logic out of the shared split_message helper (used by Signal, Slack, Discord, Weixin, etc.) and into a Telegram-specific _split_telegram_markdown function. The shared split_message remains a plain-text chunker. The Telegram channel now uses _split_telegram_markdown for its raw Markdown paths that feed _markdown_to_telegram_html, preventing broken HTML rendering when splits fall inside fenced code blocks. Also fixes a regression where content beginning with whitespace before a fence could emit a whitespace-only chunk. Addresses review feedback on #4257.
This commit is contained in:
@@ -43,6 +43,79 @@ TELEGRAM_HTML_MAX_LEN = 4096
|
||||
TELEGRAM_REPLY_CONTEXT_MAX_LEN = TELEGRAM_MAX_MESSAGE_LEN # Max length for reply context in user message
|
||||
|
||||
|
||||
def _split_telegram_markdown(content: str, max_len: int) -> list[str]:
|
||||
"""Split raw Telegram Markdown without leaving fenced code blocks unbalanced."""
|
||||
if not content:
|
||||
return []
|
||||
content = content.lstrip()
|
||||
if not content:
|
||||
return []
|
||||
if len(content) <= max_len:
|
||||
return [content]
|
||||
|
||||
def fence_line(fence_pos: int) -> str:
|
||||
line_end = content.find("\n", fence_pos)
|
||||
if line_end < 0:
|
||||
return content[fence_pos:]
|
||||
return content[fence_pos:line_end]
|
||||
|
||||
def split_inside_fenced_code_block(pos: int) -> tuple[bool, int, str]:
|
||||
if content[:pos].count("```") % 2 == 0:
|
||||
return False, -1, ""
|
||||
opening = content.rfind("```", 0, pos)
|
||||
if opening < 0:
|
||||
return True, -1, "```"
|
||||
return True, opening, fence_line(opening)
|
||||
|
||||
chunks: list[str] = []
|
||||
while content:
|
||||
if len(content) <= max_len:
|
||||
chunks.append(content)
|
||||
break
|
||||
|
||||
cut = content[:max_len]
|
||||
pos = cut.rfind("\n")
|
||||
if pos <= 0:
|
||||
pos = cut.rfind(" ")
|
||||
if pos <= 0:
|
||||
pos = max_len
|
||||
|
||||
inside_code, opening, fence = split_inside_fenced_code_block(pos)
|
||||
if inside_code:
|
||||
if opening > 0:
|
||||
pos = opening
|
||||
else:
|
||||
closing = "\n```"
|
||||
min_code_pos = len(fence)
|
||||
if content.startswith(fence + "\n"):
|
||||
min_code_pos += 1
|
||||
if pos < min_code_pos and min_code_pos + len(closing) > max_len:
|
||||
chunks.append(content[:max_len])
|
||||
content = content[max_len:].lstrip()
|
||||
continue
|
||||
if pos + len(closing) > max_len:
|
||||
budget = max_len - len(closing)
|
||||
if budget > 0:
|
||||
recut = content[:budget]
|
||||
adjusted = recut.rfind("\n")
|
||||
if adjusted <= 0:
|
||||
adjusted = recut.rfind(" ")
|
||||
pos = adjusted if adjusted > 0 else budget
|
||||
else:
|
||||
closing = "```"
|
||||
pos = max_len - len(closing)
|
||||
chunks.append(content[:pos] + closing)
|
||||
remainder = content[pos:]
|
||||
if remainder.startswith("\n"):
|
||||
remainder = remainder[1:]
|
||||
content = f"{fence}\n{remainder}"
|
||||
continue
|
||||
|
||||
chunks.append(content[:pos])
|
||||
content = content[pos:].lstrip()
|
||||
return chunks
|
||||
|
||||
|
||||
def _escape_telegram_html(text: str) -> str:
|
||||
"""Escape text for Telegram HTML parse mode."""
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
@@ -632,7 +705,7 @@ class TelegramChannel(BaseChannel):
|
||||
# Fallback: no native keyboard → splice labels into the message so the choices survive.
|
||||
if buttons and reply_markup is None:
|
||||
text = f"{text}\n\n{self._buttons_as_text(buttons)}"
|
||||
chunks = split_message(text, TELEGRAM_MAX_MESSAGE_LEN)
|
||||
chunks = _split_telegram_markdown(text, TELEGRAM_MAX_MESSAGE_LEN)
|
||||
for i, chunk in enumerate(chunks):
|
||||
is_last = (i == len(chunks) - 1)
|
||||
await self._send_text(
|
||||
@@ -838,7 +911,7 @@ class TelegramChannel(BaseChannel):
|
||||
intermediate chunks as standalone messages, then opens a new message
|
||||
for the tail so subsequent deltas continue streaming into it.
|
||||
"""
|
||||
chunks = split_message(buf.text, TELEGRAM_MAX_MESSAGE_LEN)
|
||||
chunks = _split_telegram_markdown(buf.text, TELEGRAM_MAX_MESSAGE_LEN)
|
||||
if len(chunks) <= 1:
|
||||
return
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user