from __future__ import annotations import re _ALLOWED_TAGS = { "b", "strong", "i", "em", "u", "ins", "s", "strike", "del", "code", "pre", "a", } _TAG_PATTERN = re.compile(r"]*>") _TABLE_ROW_PATTERN = re.compile(r"^\|(.+)\|$", re.MULTILINE) _TABLE_SEPARATOR_PATTERN = re.compile(r"^\|[\s\-:|]+\|$", re.MULTILINE) def text_sanitizer(text: str) -> str: if not text: return "" text = text.replace(" ", " ") def _replace_tag(m: re.Match) -> str: tag = m.group(1).lower() if tag in _ALLOWED_TAGS: return m.group(0) return "" text = _TAG_PATTERN.sub(_replace_tag, text) text = text.strip() return text def markdown_to_whatsapp(md_text: str) -> str: if not md_text: return "" text = md_text text = re.sub(r"```(\w+)?\n(.*?)```", _code_block, text, flags=re.DOTALL) text = re.sub(r"`([^`]+)`", r"```\1```", text) text = _convert_tables(text) # All formatting -> temp markers to prevent cross-contamination text = re.sub(r"\*\*\*(.+?)\*\*\*", r"\1", text) text = re.sub(r"___(.+?)___", r"\1", text) text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) text = re.sub(r"__(.+?)__", r"\1", text) text = re.sub(r"(?\1", text) text = re.sub(r"(?\1", text) text = text.replace("", "*_") text = text.replace("", "_*") text = text.replace("", "*") text = text.replace("", "*") text = text.replace("", "_") text = text.replace("", "_") text = re.sub(r"~~(.+?)~~", r"~\1~", text) text = re.sub(r"^### (.+)$", r"*_\1_*", text, flags=re.MULTILINE) text = re.sub(r"^## (.+)$", r"*_\1_*", text, flags=re.MULTILINE) text = re.sub(r"^# (.+)$", r"*_\1_*", text, flags=re.MULTILINE) text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _link, text) text = re.sub(r"^\- (.+)$", r"• \1", text, flags=re.MULTILINE) text = re.sub(r"^\d+\. (.+)$", r"• \1", text, flags=re.MULTILINE) text = text.replace("
", "\n") text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() def _convert_tables(text: str) -> str: def _replace_table(m: re.Match) -> str: table_block = m.group(0) lines = table_block.strip().split("\n") if len(lines) < 2: return table_block data_lines = [] header = None for line in lines: stripped = line.strip() if not stripped.startswith("|"): continue if _TABLE_SEPARATOR_PATTERN.match(stripped): continue cells = [c.strip() for c in stripped[1:-1].split("|")] if header is None: header = " | ".join(cells) else: data_lines.append("• " + " | ".join(cells)) if header is None: return table_block result = [header] result.extend(data_lines) return "\n".join(result) text = re.sub( r"(?:^|\n)(\|.+\|\s*\n){2,}", _replace_table, text, flags=re.MULTILINE, ) return text def _code_block(m: re.Match) -> str: lang = m.group(1) or "" code = m.group(2).strip() prefix = f"[{lang}]\n" if lang else "" return f"```{prefix}{code}```" def _link(m: re.Match) -> str: text = m.group(1) url = m.group(2) if text == url: return url if not text.strip(): return url return f"{text}\n({url})"