from __future__ import annotations import re from html import escape _HTML_TAGS = [ ("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("
", "
"), ("", ""), ("
", "
"), (""), ] _RE_MARKDOWN_TO_HTML = [ (re.compile(r"\*\*\*(.+?)\*\*\*"), r"\1"), (re.compile(r"\*\*_(.+?)_\*\*"), r"\1"), (re.compile(r"_\*\*(.+?)\*\*_"), r"\1"), (re.compile(r"\*\*(.+?)\*\*"), r"\1"), (re.compile(r"__(.+?)__"), r"\1"), (re.compile(r"~~(.+?)~~"), r"\1"), (re.compile(r"\|\|(.+?)\|\|"), r"\1"), (re.compile(r"(?\1"), (re.compile(r"(?\1"), (re.compile(r"(?\1"), (re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'\1'), ] _BULLET_LINE_RE = re.compile(r"^(\s*)\*\s", re.MULTILINE) _BLOCKQUOTE_RE = re.compile(r"^> (.+)$", re.MULTILINE) _CODE_BLOCK_RE = re.compile(r"```(\w+)?\s*\n?(.+?)```", re.DOTALL) def markdown_to_telegram_html(md_text: str) -> str: text = md_text code_blocks = [] def _save_code_block(m: re.Match) -> str: lang = m.group(1) or "" code = m.group(2) code_blocks.append(f"
{escape(code)}
") return f"\x00CODEBLOCK{len(code_blocks) - 1}\x00" text = _CODE_BLOCK_RE.sub(_save_code_block, text) text = escape(text) for pattern, replacement in _RE_MARKDOWN_TO_HTML: try: text = pattern.sub(replacement, text) except re.error: pass text = _BULLET_LINE_RE.sub(r"\1• ", text) text = _BLOCKQUOTE_RE.sub(r"
\1
", text) text = text.replace("*", "").replace("_", "") for i, block in enumerate(code_blocks): text = text.replace(f"\x00CODEBLOCK{i}\x00", block) return text.strip() def split_telegram_html_chunks(html: str, limit: int = 4096) -> list[str]: if len(html) <= limit: return [html] chunks: list[str] = [] tag_stack: list[str] = [] start = 0 i = 0 while i < len(html): if i - start >= limit: boundary = _find_safe_boundary(html, i, tag_stack) chunk = html[start:boundary] chunk = _close_open_tags(chunk, tag_stack) chunks.append(chunk) start = _skip_whitespace(html, boundary) i = start continue i += 1 if start < len(html): chunk = html[start:] chunk = _close_open_tags(chunk, tag_stack) chunks.append(chunk) return chunks def _find_safe_boundary(html: str, pos: int, tag_stack: list[str]) -> int: end = min(pos, len(html)) for i in range(end - 1, max(pos - 200, 0), -1): if html[i] == "\n": return i return end def _close_open_tags(chunk: str, tag_stack: list[str]) -> str: result = chunk for tag in reversed(tag_stack): close_tag = {"": "", "": "", "": "", "": "", "": "", "
": "
", '', "": "", "
": "
", ""}.get(tag, "") if close_tag: result += close_tag return result def _skip_whitespace(html: str, pos: int) -> int: while pos < len(html) and html[pos] in (" ", "\n", "\r", "\t"): pos += 1 return pos