from __future__ import annotations import re from html import escape _ESCAPE_CHARS = {"&": "&", "<": "<", ">": ">"} _TAG_PATTERN = r"<(?!/?(?:b|strong|i|em|u|ins|s|strike|del|code|pre|a|tg-spoiler|tg-emoji)\b)[^>]*>" _TAG_PLACEHOLDER_PATTERN = re.compile(_TAG_PATTERN) _CODE_BLOCK_RE = re.compile(r"```(?:\w+)?\n.+?```", re.DOTALL) _INLINE_CODE_RE = re.compile(r"`([^`\n]+?)`") _PLACEHOLDER_PREFIX = "\x00MDPLACEHOLDER" def markdown_to_html(text: str) -> str: code_blocks: dict[str, str] = {} text = _protect_code_spans(text, _CODE_BLOCK_RE, code_blocks) text = _protect_code_spans(text, _INLINE_CODE_RE, code_blocks) text = _convert_bold(text) text = _convert_italic(text) text = _convert_strikethrough(text) text = _convert_underline(text) text = _convert_links(text) text = _convert_spoiler(text) text = _restore_code_spans(text, code_blocks) text = _sanitize_html(text) return text def strip_all_tags(text: str) -> str: return re.sub(r"<[^>]+>", "", text) def strip_html_tags(text: str) -> str: return strip_all_tags(text) def _protect_code_spans(text: str, pattern: re.Pattern, storage: dict[str, str]) -> str: result = [] idx = 0 for m in pattern.finditer(text): result.append(text[idx : m.start()]) placeholder = f"{_PLACEHOLDER_PREFIX}{len(storage)}" storage[placeholder] = m.group(0) result.append(placeholder) idx = m.end() result.append(text[idx:]) return "".join(result) def _restore_code_spans(text: str, storage: dict[str, str]) -> str: result = text for placeholder, code_text in storage.items(): if placeholder.startswith(f"{_PLACEHOLDER_PREFIX}") and "```" in code_text: inner = code_text[3:].rstrip("`").lstrip("\n") lang_end = inner.find("\n") inner = inner[lang_end + 1 :] if lang_end >= 0 else inner safe = escape(inner, quote=False) result = result.replace(placeholder, f"
{safe}
") elif placeholder in result: inner = code_text.strip("`") safe = escape(inner, quote=False) result = result.replace(placeholder, f"{safe}") return result def _convert_bold(text: str) -> str: return re.sub(r"\*\*(.+?)\*\*", r"\1", text) def _convert_italic(text: str) -> str: text = re.sub(r"(?\1", text) text = re.sub(r"(?\1", text) return text def _convert_strikethrough(text: str) -> str: return re.sub(r"~~(.+?)~~", r"\1", text) def _convert_underline(text: str) -> str: return re.sub(r"__(\w.*?\w)__", r"\1", text) def _convert_links(text: str) -> str: return re.sub(r"\[(.+?)\]\((.+?)\)", r'\1', text) def _convert_spoiler(text: str) -> str: return re.sub(r"\|\|(.+?)\|\|", r"\1", text) def _sanitize_html(text: str) -> str: result = [] depth = 0 for ch in text: if ch == "<": depth += 1 elif ch == ">": depth = max(0, depth - 1) elif depth == 0: if ch in _ESCAPE_CHARS: result.append(_ESCAPE_CHARS[ch]) continue result.append(ch) return "".join(result)