""" Convert AI markdown output → Telegram HTML. Supports: **bold**, *italic*, `code`, ```blocks```, ~~strike~~, ||spoiler||, [links](url), # headers, bullet lists, horizontal rules, markdown tables. """ import re from html import escape as _esc def _split_table_row(line: str) -> list[str]: return [_plain_table_cell(cell.strip()) for cell in line.strip().strip("|").split("|")] def _plain_table_cell(cell: str) -> str: cell = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", cell) cell = re.sub(r"(```|`|\*\*|__|\*|_|~~|\|\|)", "", cell) return cell.strip() def _is_table_separator(line: str) -> bool: cells = _split_table_row(line) return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell.strip()) for cell in cells) def _render_table(lines: list[str]) -> str: rows = [_split_table_row(line) for line in lines if not _is_table_separator(line)] if not rows: return "\n".join(lines) width = max(len(row) for row in rows) normalized = [row + [""] * (width - len(row)) for row in rows] col_widths = [max(len(row[i]) for row in normalized) for i in range(width)] rendered = [] for idx, row in enumerate(normalized): rendered.append(" ".join(row[i].ljust(col_widths[i]) for i in range(width)).rstrip()) if idx == 0 and len(normalized) > 1: rendered.append(" ".join("-" * col_widths[i] for i in range(width)).rstrip()) return "```\n" + "\n".join(rendered) + "\n```" def _normalize_tables(text: str) -> str: lines = text.splitlines() out: list[str] = [] i = 0 while i < len(lines): if ( "|" in lines[i] and i + 1 < len(lines) and "|" in lines[i + 1] and _is_table_separator(lines[i + 1]) ): block = [lines[i], lines[i + 1]] i += 2 while i < len(lines) and "|" in lines[i] and lines[i].strip(): block.append(lines[i]) i += 1 out.append(_render_table(block)) continue out.append(lines[i]) i += 1 return "\n".join(out) def _protect(text: str) -> tuple[str, list[str]]: """Extract code blocks/inline code and replace with placeholders.""" saved: list[str] = [] def _save_block(m: re.Match) -> str: lang = (m.group(1) or "").strip() code = _esc(m.group(2)) tag = f'
{code}' if lang else f"{code}"
saved.append(tag)
return f"\x00BLK{len(saved)-1}\x00"
def _save_inline(m: re.Match) -> str:
saved.append(f"{_esc(m.group(1))}")
return f"\x00INL{len(saved)-1}\x00"
text = re.sub(r"```(\w*)\n?(.*?)```", _save_block, text, flags=re.DOTALL)
text = re.sub(r"`([^`\n]+)`", _save_inline, text)
return text, saved
def _restore(text: str, saved: list[str]) -> str:
for i, block in enumerate(saved):
text = text.replace(f"\x00BLK{i}\x00", block)
text = text.replace(f"\x00INL{i}\x00", block)
return text
def md_to_tg(text: str) -> str:
"""Convert AI markdown response to Telegram-safe HTML."""
text = _normalize_tables(text)
text, saved = _protect(text)
# Escape HTML special chars in non-code content
text = _esc(text)
# ── Block-level ──────────────────────────────────────────
# Headers → bold line
text = re.sub(r"^#{1,6}\s+(.+)$", r"\1", text, flags=re.MULTILINE)
# Horizontal rule
text = re.sub(r"^[-*_]{3,}$", "────────────────", text, flags=re.MULTILINE)
# ── Inline ───────────────────────────────────────────────
# Bold **text** or __text__
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text, flags=re.DOTALL)
text = re.sub(r"__(.+?)__", r"\1", text)
# Italic *text* or _text_ (don't match ** or __)
text = re.sub(r"(?\1", text)
text = re.sub(r"(?\1", text)
# Strikethrough ~~text~~
text = re.sub(r"~~(.+?)~~", r"