import re MAX_MSG_LEN = 4000 def _escape_md2(text: str) -> str: """Escape special chars for MarkdownV2, preserving bold/headers/bullets.""" # We use HTML instead — MarkdownV2 escaping is too fragile raise NotImplementedError("Use HTML mode") def _md_to_html(text: str) -> str: """Minimal Markdown-to-HTML for Telegram's supported subset.""" # Bold: **text** or __text__ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) text = re.sub(r"__(.+?)__", r"\1", text) # Italic: *text* (but not inside bold tags) text = re.sub(r"(?)\*(.+?)\*(?!<)", r"\1", text) # Inline code text = re.sub(r"`(.+?)`", r"\1", text) # Headers: ## Title -> bold text = re.sub(r"^#{1,3}\s+(.+)$", r"\1", text, flags=re.MULTILINE) # Escape HTML entities in remaining text (but preserve our tags) # Already handled — Telegram is lenient with unescaped text in HTML mode return text def split_report(report: str) -> list[str]: """Split a report into Telegram-safe HTML messages.""" html = _md_to_html(report) if len(html) <= MAX_MSG_LEN: return [html] # Split on section boundaries (bold headers on their own line) sections = re.split(r"(?=\n[^<]+\n)", html) messages: list[str] = [] current = "" for section in sections: if not section.strip(): continue if len(current) + len(section) > MAX_MSG_LEN: if current.strip(): messages.append(current.strip()) # If a single section is too long, split on paragraphs if len(section) > MAX_MSG_LEN: paragraphs = section.split("\n\n") current = "" for para in paragraphs: if len(current) + len(para) + 2 > MAX_MSG_LEN: if current.strip(): messages.append(current.strip()) current = para + "\n\n" else: current += para + "\n\n" else: current = section else: current += section if current.strip(): messages.append(current.strip()) return messages if messages else [html[:MAX_MSG_LEN]]